# 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' → (renders inside a centered card)
(anything else) → AppSidebar +
```
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: `` + 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
```
### How to add AdminLayout without breaking anything
Extend the `v-if` chain to a three-way switch using `route.meta.layout`:
```html
```
`AdminLayout.vue` renders its own sidebar and a ``. 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)
└──
```
`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 `