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>
509 lines
19 KiB
Markdown
509 lines
19 KiB
Markdown
# 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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
# 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):
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
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`:
|
|
|
|
```html
|
|
<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`:
|
|
|
|
```html
|
|
<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:
|
|
|
|
```js
|
|
{ path: '/admin', component: () => import('../views/AdminView.vue'), meta: { requiresAdmin: true } }
|
|
```
|
|
|
|
Replace with a route subtree:
|
|
|
|
```js
|
|
{
|
|
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`:
|
|
|
|
```js
|
|
// 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:
|
|
|
|
```js
|
|
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:
|
|
|
|
```js
|
|
// 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`:
|
|
|
|
```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
|
|
```
|
|
|
|
```html
|
|
<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)
|