Files
kite/.planning/research/ARCHITECTURE.md
T
curo1305andClaude Sonnet 4.6 f7758776ec docs: define milestone v0.2 requirements (36 reqs, 6 categories)
UI overhaul, codebase quality, admin panel rearchitecture, UX
improvements, responsive layout, and performance/stack updates.
Research: STACK.md, FEATURES.md, ARCHITECTURE.md, PITFALLS.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 09:22:40 +02:00

19 KiB

Architecture Patterns

Domain: v0.2 UI Overhaul and Optimization — integration analysis for existing DocuVault codebase Researched: 2026-06-07


How the Layout System Currently Works

App.vue is the layout switch. It checks route.meta.layout and renders one of two branches:

route.meta.layout === 'auth'  →  <AuthLayout />   (renders <router-view /> inside a centered card)
(anything else)               →  AppSidebar + <main><router-view /></main>

There are currently only two layouts:

  • AuthLayout.vue — centered card, no sidebar, used for login/register/password-reset
  • Implicit main layout — App.vue's v-else branch: <AppSidebar> + scrollable main

The admin panel (/admin) currently falls into the v-else branch. AdminView.vue renders inside the main user sidebar, inheriting AppSidebar's folder tree, quota bar, and user nav — none of which are relevant to an admin.


Integration Point 1: FastAPI Admin Router Decomposition

Current state

backend/api/admin.py is a single file with router = APIRouter(prefix="/api/admin"). It is registered in main.py as:

from api.admin import router as admin_router
app.include_router(admin_router)

The file mixes four concern areas:

  • User management (CRUD on users, status, password reset)
  • Quota management (get/patch quota per user)
  • AI provider configuration (system-level: get, put, test-connection, get-models)
  • Audit log was already extracted to api/audit.py in v0.1

Target structure

backend/api/admin/
    __init__.py
    users.py    — /api/admin/users endpoints
    quotas.py   — /api/admin/users/{id}/quota endpoints
    ai.py       — /api/admin/ai-config endpoints

How to register without changing URL prefix

FastAPI supports two-level include: a parent router with the shared prefix, sub-routers without prefix. This is the correct pattern:

# backend/api/admin/__init__.py
from fastapi import APIRouter
from .users import router as users_router
from .quotas import router as quotas_router
from .ai import router as ai_router

router = APIRouter(prefix="/api/admin", tags=["admin"])
router.include_router(users_router)
router.include_router(quotas_router)
router.include_router(ai_router)

Each sub-router declares its routes WITHOUT a prefix (no prefix= arg):

# backend/api/admin/users.py
router = APIRouter()

@router.get("/users")           # resolves to /api/admin/users
@router.post("/users")          # resolves to /api/admin/users
@router.patch("/users/{id}/status")
...

main.py changes from:

from api.admin import router as admin_router
app.include_router(admin_router)

to the identical line — because api/admin/__init__.py exports router with the same name. No URL changes. No consumer changes.

What moves where

Endpoints Target file Pydantic models that move with it
GET/POST /users, PATCH /users/{id}/status, POST /users/{id}/password-reset, DELETE /users/{id} admin/users.py UserCreate, UserStatusUpdate, UserDeleteConfirm, _user_to_dict
GET/PATCH /users/{id}/quota, PATCH /users/{id}/ai-config admin/quotas.py QuotaUpdate, UserAiConfigUpdate
GET/PUT /ai-config, POST /ai-config/test-connection, GET /ai-config/models admin/ai.py SystemAiConfigUpdate, TestConnectionRequest, _ai_config_to_dict

POST /topics (create system topic) stays in admin/users.py — it is short and has no unique models. Alternatively it can go in its own admin/topics.py; a decision either way is fine.

Shared helpers

_DEFAULT_QUOTA_BYTES, CloudConnectionOut, and any imports shared across sub-routers can live in backend/api/admin/shared.py and be imported by each sub-router. Do not duplicate them.

Files changed

File Change type
backend/api/admin.py DELETED — replaced by package
backend/api/admin/__init__.py CREATED — aggregating router
backend/api/admin/users.py CREATED
backend/api/admin/quotas.py CREATED
backend/api/admin/ai.py CREATED
backend/api/admin/shared.py CREATED (optional, holds shared constants/helpers)
backend/main.py NO CHANGE — import path api.admin.router remains valid

Integration Point 2: Vue Admin Layout

Current layout switch

App.vue:

<AuthLayout v-if="route.meta.layout === 'auth'" />
<div v-else class="flex h-screen overflow-hidden">
  <AppSidebar />
  <main class="flex-1 overflow-y-auto">
    <router-view />
  </main>
</div>

How to add AdminLayout without breaking anything

Extend the v-if chain to a three-way switch using route.meta.layout:

<AuthLayout v-if="route.meta.layout === 'auth'" />
<AdminLayout v-else-if="route.meta.layout === 'admin'" />
<div v-else class="flex h-screen overflow-hidden">
  <AppSidebar />
  <main class="flex-1 overflow-y-auto">
    <router-view />
  </main>
</div>

AdminLayout.vue renders its own sidebar and a <router-view />. It is structurally identical in shape to the main layout but with admin-specific nav items.

Router changes

Current single admin route:

{ path: '/admin', component: () => import('../views/AdminView.vue'), meta: { requiresAdmin: true } }

Replace with a route subtree:

{
  path: '/admin',
  component: AdminLayout,
  meta: { requiresAdmin: true, layout: 'admin' },
  children: [
    { path: '', redirect: '/admin/users' },
    { path: 'users',  component: () => import('../views/admin/AdminUsersView.vue') },
    { path: 'quotas', component: () => import('../views/admin/AdminQuotasView.vue') },
    { path: 'ai',     component: () => import('../views/admin/AdminAiView.vue') },
    { path: 'audit',  component: () => import('../views/admin/AdminAuditView.vue') },
  ],
}

Meta inheritance caveat

Vue Router 4 does NOT automatically merge meta from parent to children — route.meta on a child route only contains what is declared on that child. App.vue currently reads route.meta.layout directly. For the child routes to inherit layout: 'admin', the check in App.vue must walk route.matched:

// App.vue — layout resolution
const layout = computed(() => route.matched.find(r => r.meta.layout)?.meta.layout)

Likewise, the navigation guard in router/index.js must change from to.meta.requiresAdmin to to.matched.some(r => r.meta.requiresAdmin). The current guard already handles .meta.public the same way — so this is a one-line change pattern that's already established in the router.

AdminLayout.vue structure

AdminLayout.vue
  └── AdminSidebar.vue (nav links: Users, Quotas, AI Config, Audit Log)
  └── <router-view />

AdminSidebar.vue is NOT AppSidebar.vue. Do not reuse or extend AppSidebar. The admin sidebar is a new, independent component with admin-specific nav.

Views created

Each current tab component becomes a thin view wrapper (following the View -> Smart -> Presentational hierarchy):

New file Wraps existing component
views/admin/AdminUsersView.vue AdminUsersTab.vue (kept unchanged as smart component)
views/admin/AdminQuotasView.vue AdminQuotasTab.vue
views/admin/AdminAiView.vue AdminAiConfigTab.vue
views/admin/AdminAuditView.vue AuditLogTab.vue

The existing tab components (AdminUsersTab.vue, AdminQuotasTab.vue, etc.) are NOT renamed or restructured in this step. They become the smart components rendered inside the new thin view wrappers.

Files changed

File Change type
frontend/src/App.vue MODIFIED — add v-else-if for admin layout; meta layout resolution uses route.matched
frontend/src/router/index.js MODIFIED — /admin replaced with children subtree; guard uses to.matched
frontend/src/layouts/AdminLayout.vue CREATED
frontend/src/components/layout/AdminSidebar.vue CREATED
frontend/src/views/AdminView.vue DELETED — replaced by subtree
frontend/src/views/admin/AdminUsersView.vue CREATED (thin wrapper)
frontend/src/views/admin/AdminQuotasView.vue CREATED (thin wrapper)
frontend/src/views/admin/AdminAiView.vue CREATED (thin wrapper)
frontend/src/views/admin/AdminAuditView.vue CREATED (thin wrapper)
frontend/src/components/admin/AdminUsersTab.vue KEPT (smart component, unchanged)
frontend/src/components/admin/AdminQuotasTab.vue KEPT
frontend/src/components/admin/AdminAiConfigTab.vue KEPT
frontend/src/components/admin/AuditLogTab.vue KEPT

Integration Point 3: client.js Decomposition

Current import pattern

Every consumer does one of:

import * as api from '../api/client.js'          // namespace import — stores, views, most components
import { specificFn } from '../api/client.js'    // named import — a few components

35+ files import from client.js. A decomposition that changes the import path in all 35 files is a large, purely mechanical refactor with meaningful test/review cost for zero user-visible benefit.

Minimal, non-breaking approach: barrel re-export

Create domain modules alongside client.js, then have client.js re-export everything so no consumer changes:

frontend/src/api/
    client.js          — request() transport function + re-exports from domain files
    documents.js       — document API calls (import request from './client.js')
    topics.js
    auth.js
    admin.js
    folders.js
    shares.js
    cloud.js

client.js becomes the HTTP transport layer and a re-export barrel:

// client.js (after decomposition) — transport only + re-exports
async function request(path, options = {}) { ... }
export async function fetchDocumentContent(docId, options = {}) { ... }

export * from './documents.js'
export * from './topics.js'
export * from './auth.js'
export * from './admin.js'
export * from './folders.js'
export * from './shares.js'
export * from './cloud.js'

Each domain file imports request from client.js:

// documents.js
import { request } from './client.js'
export function listDocuments(...) { return request('/api/documents?...') }

This means import * as api from '../api/client.js' continues to work for all 35 existing consumers. The decomposition is fully internal.

Test mock compatibility

Existing Vitest tests mock api/client.js at the module level (vi.mock('../../api/client.js')). Because the re-exports are in client.js, mocking client.js still intercepts all domain functions. No test changes needed.

Files changed

File Change type
frontend/src/api/client.js MODIFIED — split out domain functions, keep re-exports
frontend/src/api/documents.js CREATED
frontend/src/api/topics.js CREATED
frontend/src/api/auth.js CREATED (name does not conflict with stores/auth.js — different path)
frontend/src/api/admin.js CREATED
frontend/src/api/folders.js CREATED
frontend/src/api/shares.js CREATED
frontend/src/api/cloud.js CREATED
All 35 consumer files NO CHANGE

Integration Point 4: Icon Strategy

Current state

There are 58 inline <svg> blocks in frontend/src/components/ and 8 more in frontend/src/views/. Each is a 3-6 line block with stroke, viewBox, class, and a <path>. The same path data appears multiple times for the same icon (e.g., the document icon in the sidebar, document card, and document view are separate copies of the same path string).

Options assessed

Option A: SVG sprite sheet A <symbol>-based sprite in index.html with <use href="#icon-name" /> in components. Minimal bundle impact, no component overhead. Drawback: requires a build step or manual maintenance of the sprite file; poor DX for adding new icons during the visual redesign.

Option B: AppIcon.vue component with a name prop A Vue component that maps name to an inline <svg> block. All SVG paths live in one file. Zero external dependencies. Easy to add icons.

Option C: lucide-vue-next Purpose-built icon library for Tailwind projects. Tree-shakeable individual icon components: <FileText class="w-4 h-4" />. Maintained, typed, comprehensive. Avoids maintaining path data entirely.

Recommendation: Option B (AppIcon.vue) unless a dependency is explicitly approved.

The project already has 66 SVG paths hand-chosen to match the visual style. Switching to lucide means auditing that every lucide icon matches the expected visual. During a visual redesign milestone that may be desirable, but it is a separate decision. The minimal refactor that eliminates duplication without changing visual output is Option B.

Named slots are wrong for this problem. Named slots solve "inject different markup into different zones of a layout". This problem is "render the correct path data for a given icon name". A name prop is the correct API.

Option B implementation:

frontend/src/components/ui/AppIcon.vue
<template>
  <svg :class="$attrs.class" fill="none" stroke="currentColor" viewBox="0 0 24 24"
       aria-hidden="true">
    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="paths[name]" />
  </svg>
</template>
<script>
export default {
  inheritAttrs: false,
  props: { name: { type: String, required: true } },
  data: () => ({
    paths: {
      document: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5...',
      folder: 'M3 7a2 2 0 012-2h4l2 2h6a2 2 0 012 2v...',
      // etc.
    }
  })
}
</script>

Usage: <AppIcon name="document" class="w-4 h-4 mr-2 shrink-0" />

Files changed (Option B)

File Change type
frontend/src/components/ui/AppIcon.vue CREATED
~35 components/views with inline SVGs MODIFIED — replace <svg>...</svg> with <AppIcon name="..." class="..." />

Suggested Build Order

Dependencies between the four changes determine order. A change that other changes depend on must land first.

Step 1 — Backend admin router decomposition
Step 2 — API client decomposition (client.js -> domain files)
Step 3 — Admin layout + route subtree (frontend)
Step 4 — Icon refactor

Rationale

Step 1 (backend) first:

  • No frontend dependencies. All existing tests pass with zero behavior change (same URLs, same auth, same import path in main.py).
  • Risk: lowest. Pure internal restructuring.

Step 2 (client.js) second:

  • No dependency on the admin layout.
  • New admin view files created in Step 3 can import from api/admin.js specifically rather than the monolith.
  • Internal-only change. All 35 consumer files unchanged. All existing mock-based tests unchanged.
  • Risk: low.

Step 3 (admin layout + route subtree) third:

  • Highest risk: modifies App.vue (central layout switch), router/index.js (all navigation), creates new layout files.
  • The existing smart components (AdminUsersTab.vue etc.) are not touched — they are simply wrapped by new thin view files.
  • Navigation guard logic for requiresAdmin and layout meta must be re-tested after this step.
  • Risk: medium.

Step 4 (icon refactor) last:

  • Pure cosmetic. Zero behavior change.
  • Can be done incrementally (file by file) without blocking anything.
  • Risk: lowest.

Parallelism

Steps 1 and 2 are independent and can be executed simultaneously — they touch no shared files.

Steps 3 and 4 overlap on the admin component files (icons are replaced in Step 4, structure is set in Step 3). Complete Step 3 first to avoid merge conflicts.


Component Architecture: Existing vs. Target

Current (wrong)

App.vue
  └── AppSidebar (always present)
  └── AdminView.vue (tabs rendered in user sidebar context)
        └── AdminUsersTab.vue
        └── AdminQuotasTab.vue
        └── AdminAiConfigTab.vue
        └── AuditLogTab.vue

Target (correct)

App.vue (layout switch)
  └── AuthLayout.vue  (for meta.layout === 'auth')
  └── AdminLayout.vue (for meta.layout === 'admin')
        └── AdminSidebar.vue
        └── <router-view />
              → AdminUsersView.vue  (thin — passes props to AdminUsersTab)
              → AdminQuotasView.vue
              → AdminAiView.vue
              → AdminAuditView.vue
  └── Implicit main layout (v-else)
        └── AppSidebar.vue
        └── <router-view />
              → FileManagerView, DocumentView, TopicsView, ...

The smart components (AdminUsersTab.vue etc.) are unchanged. The view layer above them is what changes.


What Is New vs. Modified: Complete Inventory

New files

backend/api/admin/__init__.py
backend/api/admin/users.py
backend/api/admin/quotas.py
backend/api/admin/ai.py
backend/api/admin/shared.py          (optional — shared constants/helpers)

frontend/src/layouts/AdminLayout.vue
frontend/src/components/layout/AdminSidebar.vue
frontend/src/views/admin/AdminUsersView.vue
frontend/src/views/admin/AdminQuotasView.vue
frontend/src/views/admin/AdminAiView.vue
frontend/src/views/admin/AdminAuditView.vue
frontend/src/api/documents.js
frontend/src/api/topics.js
frontend/src/api/auth.js
frontend/src/api/admin.js
frontend/src/api/folders.js
frontend/src/api/shares.js
frontend/src/api/cloud.js
frontend/src/components/ui/AppIcon.vue  (Option B only)

Modified files

backend/main.py                           (no functional change — import stays identical)

frontend/src/App.vue                      (add v-else-if; use route.matched for layout)
frontend/src/router/index.js              (admin route becomes children subtree; guard uses to.matched)
frontend/src/api/client.js               (gutted to transport + re-exports)
All ~35 components with inline SVGs      (icons replaced with AppIcon or lucide)

Deleted files

backend/api/admin.py                     (replaced by package)
frontend/src/views/AdminView.vue         (replaced by route subtree)

Minimal vs. Proper Assessment

Change Is the minimal solution also the proper one?
Backend decomposition Yes. Sub-router via __init__.py aggregator is both minimal and the standard FastAPI pattern.
client.js decomposition Yes. Re-export barrel means zero consumer churn while achieving internal separation.
Admin layout Yes. Three-way v-if in App.vue + new layout file is 20 lines of change in existing files.
Icon refactor The minimal solution (AppIcon.vue) is simpler than lucide but adds maintenance burden for the path map. lucide is more proper but adds a dependency. Call must be made by project owner.

Sources

  • Inspected source: backend/main.py, backend/api/admin.py, frontend/src/App.vue, frontend/src/router/index.js, frontend/src/layouts/AuthLayout.vue, frontend/src/api/client.js, frontend/src/views/AdminView.vue, frontend/src/components/layout/AppSidebar.vue
  • FastAPI sub-router include_router nesting: standard FastAPI router composition (HIGH confidence)
  • Vue Router 4 route.matched for meta inheritance: Vue Router 4 documented behavior (HIGH confidence)
  • lucide-vue-next: tree-shakeable Vue 3 icon library (MEDIUM confidence — verified as active, Tailwind-compatible)