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>
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-elsebranch:<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.pyin 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.jsspecifically 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.vueetc.) are not touched — they are simply wrapped by new thin view files. - Navigation guard logic for
requiresAdminandlayoutmeta 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_routernesting: standard FastAPI router composition (HIGH confidence) - Vue Router 4
route.matchedfor 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)