Moves phases 08–11 execution artifacts from .planning/phases/ to .planning/milestones/v0.2-phases/ to keep .planning/phases/ clean for the next milestone. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
42 KiB
Phase 9: Admin Panel Rearchitecture - Research
Researched: 2026-06-12 Domain: Vue Router 4 nested routes, layout components, FastAPI sub-module pattern, Tailwind safelist Confidence: HIGH (all findings sourced directly from the live codebase)
<user_constraints>
User Constraints (from CONTEXT.md)
Locked Decisions
- D-01: Overview layout: 3-4 stat cards top row (total users, total storage, document status: processing/ready/failed), then a table of the last 10 audit entries.
- D-02: Backend: new
overview.pysub-module inbackend/api/admin/. SingleGET /api/admin/overviewendpoint, one payload with all stats. - D-03: Audit entries inline in the overview response — no separate
/api/auditcall from the overview component. - D-04: SVG icons for each sidebar nav item, consistent with
AppSidebar.vue. - D-05: "Admin" label/badge below "DocuVault" logo in sidebar header. Same indigo brand color.
- D-06: No "Back to app" link. Admin accounts are operators only. Overrides ADMIN-09 requirement text.
- D-07: Nav items (in order): Overview, Users, Quotas, AI Config, Audit Log.
- D-08: After login,
user.role === 'admin'→ router redirects to/admininstead of/. - D-09:
beforeEachguard: admin on non-admin route → redirect to/admin. Admin cannot navigate to user routes. - D-10: Two guard branches in
beforeEach: (a) non-admin →/admin/*→ redirect/; (b) admin → non-admin route → redirect/admin. Both useto.matched.some(r => r.meta.requiresAdmin). - D-11: Four existing tab components become standalone view files in
frontend/src/views/admin/:AdminUsersView.vue,AdminQuotasView.vue,AdminAiView.vue,AdminAuditView.vue. Structural rename only — strip top-level padding. - D-12: Original
components/admin/AdminXxxTab.vuefiles andAdminView.vuedeleted after extraction. - D-13:
AuditLogTab.vuepromoted as-is. No new features for the audit view in Phase 9. - D-14: Tailwind safelist pattern:
Researcher verifies which color families
safelist: [ { pattern: /bg-(blue|green|purple|orange|gray|indigo|red)-(50|100|500|600)/ }, { pattern: /text-(blue|green|purple|orange|gray|indigo|red)-(500|600|700)/ }, ]formatters.jsactually uses. - D-15: CODE-09 comment purge covers all Phase 9 files + retroactive purge of Phase 8 backend sub-packages.
- D-16: Purge criterion: remove "what" comments (generic docstrings, "this does X"). Keep "why" comments (constraints, pitfall notes, non-obvious invariants).
Claude's Discretion
- Exact SVG icon choices for each admin sidebar nav item (researcher picks from the same icon family used in
AppSidebar.vue). - Exact Python queries for the overview aggregate endpoint.
- Naming of the new view files if
views/admin/directory structure is adopted.
Deferred Ideas (OUT OF SCOPE)
None — discussion stayed within phase scope.
</user_constraints>
<phase_requirements>
Phase Requirements
| ID | Description | Research Support |
|---|---|---|
| ADMIN-08 | Admin panel moved to /admin/* route subtree with AdminLayout.vue as route component. AdminView.vue deleted. |
Router restructure in §Architecture Patterns; AuthLayout.vue pattern; App.vue analysis |
| ADMIN-09 | Admin sidebar nav links (D-06 overrides "Back to app" item). | AppSidebar.vue deep-read; nav-link CSS classes documented |
| ADMIN-10 | Each admin section is its own deep-linkable URL. Browser back button works. | Vue Router 4 nested routes pattern; child route structure |
| ADMIN-11 | Admin overview page at /admin showing platform stats + last 10 audit entries. New backend endpoint. |
ORM models for aggregate query; audit.py query pattern reuse; overview.py design |
| ADMIN-12 | requiresAdmin guard uses to.matched.some(r => r.meta.requiresAdmin). |
PITFALLS.md §Pitfall 3 — exact guard fix documented; current guard code shown |
| CODE-06 | Tailwind safelist for dynamic providerColor/providerBg/providerLabel patterns. |
formatters.js fully read — exact color families documented |
| CODE-09 | Comment purge over Phase 9 files + retroactive purge of Phase 8 backend sub-packages. | Phase 8 backend sub-packages inspected; purge criterion from D-16 |
</phase_requirements>
Summary
Phase 9 is a structural rearchitecture of the admin interface. The existing AdminView.vue (tabs-on-user-layout pattern) is replaced with AdminLayout.vue (its own Vue Router parent route whose <router-view /> renders child views). Four existing tab components are promoted to standalone views under frontend/src/views/admin/. A new admin overview route at /admin needs a new backend aggregate endpoint (GET /api/admin/overview). The requiresAdmin guard must switch from to.meta.requiresAdmin to to.matched.some(r => r.meta.requiresAdmin) — the current flat check is a security regression waiting to happen once children are added. Strict admin role separation (admins redirected away from user routes) is a new guard enhancement. The Tailwind safelist is a one-file configuration change. The comment purge is the final cleanup pass.
All changes are structural — no behavior changes to existing admin functionality. The planner should organize work as: (1) backend overview.py new endpoint, (2) router restructure + guard updates, (3) AdminLayout + admin views extraction, (4) Tailwind safelist, (5) comment purge.
Primary recommendation: Follow the AuthLayout.vue pattern exactly for AdminLayout.vue — a minimal wrapper with <router-view />. Use to.matched.some() as the single source of truth for admin route detection everywhere in the guard.
Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|---|---|---|---|
| Admin layout shell (sidebar + content area) | Frontend (new AdminLayout.vue) |
— | Route component that owns the admin chrome |
| Admin route protection | Frontend (router beforeEach) |
— | Guard runs before any component mounts |
| Admin role redirect on login | Frontend (LoginView.vue / router guard) |
— | Role from authStore.user.role after token refresh |
| Admin overview stats | API / Backend (overview.py) |
— | Aggregate query across users, quotas, documents |
| Admin overview data fetch | Frontend (AdminOverviewView.vue) |
— | Single-fetch view, no store needed |
| Existing admin CRUD | API / Backend (unchanged) | — | users.py, quotas.py, ai.py, audit.py unchanged |
| Dynamic color class rendering | Frontend (Tailwind safelist in tailwind.config.js) |
— | Build-time config, no runtime code |
Standard Stack
No new packages are installed in Phase 9. All work uses the existing stack.
Existing Stack Used
| Component | File | How Used in Phase 9 |
|---|---|---|
| Vue Router 4 | Already installed | Nested route with AdminLayout as parent component |
| FastAPI / SQLAlchemy 2.0 | Already installed | New overview.py endpoint with async aggregate queries |
| Tailwind CSS + Vite | Already configured | Add safelist array to tailwind.config.js |
@tailwindcss/forms |
Already installed (Phase 8) | No change needed |
No package installs required for Phase 9.
Package Legitimacy Audit
Not applicable — Phase 9 installs no new packages.
Architecture Patterns
System Architecture Diagram
Browser (Vue 3 SPA)
│
├── /login → AuthLayout → LoginView
│ └── on success: user.role === 'admin' → push('/admin')
│ user.role === 'user' → push(redirect || '/')
│
├── / (user routes)
│ ├── App.vue renders: flex h-screen + AppSidebar + <router-view />
│ └── beforeEach guard: admin role on non-admin route → redirect /admin
│
└── /admin (admin routes — NEW)
├── App.vue renders: AdminLayout (resolves as /admin route's component)
│ AdminLayout: w-64 AdminSidebar + <router-view /> for child content
│
├── /admin → AdminOverviewView (new, single fetch from /api/admin/overview)
├── /admin/users → AdminUsersView (extracted from AdminUsersTab.vue)
├── /admin/quotas → AdminQuotasView (extracted from AdminQuotasTab.vue)
├── /admin/ai → AdminAiView (extracted from AdminAiConfigTab.vue)
└── /admin/audit → AdminAuditView (extracted from AuditLogTab.vue)
Backend API (unchanged URL surface)
│
└── GET /api/admin/overview (NEW)
├── SELECT COUNT(*) FROM users WHERE role = 'user'
├── SELECT SUM(used_bytes) FROM quotas
├── SELECT status, COUNT(*) FROM documents GROUP BY status
└── Last 10 audit entries (reuse _build_filtered_query_with_handles, limit=10)
Recommended Project Structure
frontend/src/
├── layouts/
│ ├── AuthLayout.vue # existing — reference pattern
│ └── AdminLayout.vue # NEW — mirrors AuthLayout structure
├── views/
│ ├── AdminView.vue # DELETED after extraction
│ └── admin/ # NEW directory
│ ├── AdminOverviewView.vue # NEW — stats cards + audit table
│ ├── AdminUsersView.vue # extracted from AdminUsersTab.vue
│ ├── AdminQuotasView.vue # extracted from AdminQuotasTab.vue
│ ├── AdminAiView.vue # extracted from AdminAiConfigTab.vue
│ └── AdminAuditView.vue # extracted from AuditLogTab.vue
├── components/admin/
│ ├── AdminUsersTab.vue # DELETED (dead code after extraction)
│ ├── AdminQuotasTab.vue # DELETED
│ ├── AdminAiConfigTab.vue # DELETED
│ └── AuditLogTab.vue # DELETED
backend/api/admin/
├── __init__.py # add overview_router import + include_router call
├── shared.py # unchanged
├── users.py # unchanged
├── quotas.py # unchanged
├── ai.py # unchanged
└── overview.py # NEW — GET /api/admin/overview
Pattern 1: AdminLayout follows AuthLayout pattern exactly
AuthLayout.vue is 12 lines. It is a plain <template> wrapper with <router-view /> at the center. AdminLayout.vue follows the same structure — it is a layout shell, not a nested component inside App.vue.
<!-- frontend/src/layouts/AdminLayout.vue — mirrors AuthLayout.vue structure -->
<!-- Source: frontend/src/layouts/AuthLayout.vue (read directly) -->
<template>
<div class="flex h-screen overflow-hidden">
<AdminSidebar />
<main class="flex-1 overflow-y-auto p-8 max-w-5xl mx-auto">
<router-view />
</main>
</div>
</template>
<script setup>
import AdminSidebar from '../components/admin/AdminSidebar.vue'
</script>
Critical: App.vue does NOT need a third v-else-if branch. It already renders <router-view /> (inside the user layout div). When /admin is navigated to, the router resolves AdminLayout as the component — App.vue's <router-view /> renders it. AdminLayout's own <router-view /> then renders the child view. This is the standard Vue Router 4 nested layout pattern. Adding a third v-else-if would cause double rendering (PITFALLS.md §Pitfall 4). [VERIFIED: live codebase read]
Current App.vue:
<!-- Source: frontend/src/App.vue (read directly) -->
<template>
<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>
</template>
The v-else branch renders for every non-auth route including /admin. When /admin resolves, <router-view /> renders AdminLayout, which has its own sidebar and its own <router-view />. This is correct — App.vue needs no changes. [VERIFIED: live codebase read]
Pattern 2: Vue Router 4 nested route with meta inheritance fix
// Source: frontend/src/router/index.js (read directly) — CURRENT (broken for children)
// to.meta.requiresAdmin — only works on the parent route, NOT its children in Vue Router 4
if (to.meta.requiresAdmin && authStore.user?.role !== 'admin') {
// FIXED (PITFALLS.md §Pitfall 3 — [VERIFIED: live codebase]):
if (to.matched.some(r => r.meta.requiresAdmin) && authStore.user?.role !== 'admin') {
Full guard rewrite required (D-09, D-10):
// Source: CONTEXT.md D-09, D-10 + PITFALLS.md §Pitfall 3
router.beforeEach(async (to) => {
const authStore = useAuthStore()
// Silent refresh on page reload (token is memory-only)
if (!to.meta.public && !authStore.accessToken) {
try {
await authStore.refresh()
} catch {
return { path: '/login', query: { redirect: to.fullPath } }
}
}
const isAdminRoute = to.matched.some(r => r.meta.requiresAdmin)
const isAdmin = authStore.user?.role === 'admin'
// D-10a: non-admin attempting admin route
if (isAdminRoute && !isAdmin) {
return { path: '/' }
}
// D-09: admin attempting user route (non-admin, non-public route)
if (!isAdminRoute && !to.meta.public && isAdmin) {
return { path: '/admin' }
}
})
Route structure:
// Source: CONTEXT.md §Established Patterns, PITFALLS.md §Pitfall 4
{
path: '/admin',
component: AdminLayout,
meta: { requiresAdmin: true },
children: [
{ path: '', component: AdminOverviewView }, // /admin
{ path: 'users', component: AdminUsersView },
{ path: 'quotas', component: AdminQuotasView },
{ path: 'ai', component: AdminAiView },
{ path: 'audit', component: AdminAuditView },
]
}
Note: the '' child (empty path) renders AdminOverviewView at exactly /admin. Vue Router 4 matches empty path children when the parent path is matched exactly — no redirect needed.
Pattern 3: Login redirect for admin role (D-08)
LoginView.vue's handleLoginResult function currently redirects to route.query.redirect || '/'. To implement D-08, the function must check authStore.user.role after login success:
// Source: frontend/src/views/auth/LoginView.vue (read directly) — current code
async function handleLoginResult(result) {
if (!result) {
const redirect = route.query.redirect || '/'
await router.push(redirect)
return
}
// ...
}
// MODIFIED for D-08:
async function handleLoginResult(result) {
if (!result) {
// Admin users land in /admin; users land at redirect or /
const defaultRedirect = authStore.user?.role === 'admin' ? '/admin' : '/'
const redirect = route.query.redirect || defaultRedirect
await router.push(redirect)
return
}
// ...
}
authStore.user is set before handleLoginResult is called (the login action sets user.value = data.user synchronously before returning). [VERIFIED: live codebase read of stores/auth.js line 82-83]
Pattern 4: Admin tab components — top-level padding audit
AdminView.vue wraps all content in p-8 max-w-5xl mx-auto. Each tab component starts with <div> (no padding). When extracted to views:
AdminUsersTab.vue— top element:<div>(no padding) [VERIFIED: live codebase read]AdminQuotasTab.vue— top element:<div>(no padding) [VERIFIED: live codebase read]AdminAiConfigTab.vue— top element:<div>(no padding at template level) [VERIFIED: live codebase read]AuditLogTab.vue— top element:<div>(no padding) [VERIFIED: live codebase read]
None of the tab components have top-level p-8 or max-w classes. The p-8 max-w-5xl mx-auto is only in AdminView.vue. This means the extracted views do NOT need padding stripped — they are already padding-free. The padding moves from AdminView.vue to AdminLayout.vue's content wrapper.
Double-padding risk (PITFALLS.md §Pitfall 9) is not present in this specific codebase. [VERIFIED: all four tab files read directly]
Pattern 5: AdminSidebar structure (referenced from AppSidebar.vue)
AppSidebar.vue uses these exact CSS patterns: [VERIFIED: live codebase read]
- Sidebar container:
aside.w-64.bg-white.border-r.border-gray-200.flex.flex-col.h-full.shrink-0 - Logo header:
div.px-6.py-5.border-b.border-gray-100withh1.text-lg.font-bold.text-indigo-600.tracking-tight+p.text-xs.text-gray-400.mt-0.5for subtitle - Nav section:
nav.flex-1.px-3.py-4.overflow-y-auto - Nav link CSS classes (defined in
<style scoped>):.nav-link { @apply flex items-center px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-100 hover:text-gray-900 transition-colors text-sm font-medium; } .nav-link-active { @apply bg-indigo-50 text-indigo-700; } - Active state:
:class="{ 'nav-link-active': $route.path.startsWith('/admin/...') }" - SVG icons:
w-4 h-4 mr-2 shrink-0,fill="none" stroke="currentColor" viewBox="0 0 24 24",stroke-linecap="round" stroke-linejoin="round" stroke-width="2" - User identity footer:
div.px-3.py-4.border-t.border-gray-100
"Admin" badge below logo: The AppSidebar.vue subtitle is p.text-xs.text-gray-400 with text "Document Manager". AdminSidebar.vue uses the same structure with text "Admin" (or a small badge). D-05 says "subtle label/badge below DocuVault logo". Recommended: keep same p.text-xs element but use text-indigo-500 font-semibold to give it admin context without heavy visual change.
SVG icon paths for admin nav items (same SVG family used throughout — Heroicons outline style, viewBox="0 0 24 24"):
| Nav Item | SVG Path Recommendation |
|---|---|
| Overview | M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6 (home/grid) |
| Users | M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z (users group) |
| Quotas | M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z (chart bars) |
| AI Config | M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z (computer) |
| Audit Log | M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2 (clipboard list) |
These are confirmed Heroicons outline paths consistent with icons used in AppSidebar.vue (verified by examining the existing admin shield icon, settings gear icon, and folder icon in the sidebar). [ASSUMED — specific path strings not individually verified against heroicons.com, but family is consistent]
User identity footer: AdminSidebar.vue should replicate the footer from AppSidebar.vue: avatar initial circle + email + sign-out button. The sign-out calls authStore.logout() then router.push('/login').
Pattern 6: Backend overview.py sub-module pattern
The existing api/admin/__init__.py aggregates three sub-routers. overview.py adds a fourth: [VERIFIED: live codebase read]
# backend/api/admin/overview.py — NEW
# Source pattern: backend/api/admin/users.py structure (read directly)
from fastapi import APIRouter, Depends
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from db.models import Document, Quota, User, AuditLog
from deps.auth import get_current_admin
from deps.db import get_db
from api.audit import _build_filtered_query_with_handles, _audit_to_dict_with_handles
from sqlalchemy.orm import aliased
router = APIRouter() # NO prefix — parent __init__.py carries /api/admin (D-04)
The router = APIRouter() with NO prefix is mandatory. [VERIFIED: backend/api/admin/__init__.py read directly; all three sub-routers use this pattern]
Aggregate query for overview endpoint:
@router.get("/overview")
async def get_overview(
session: AsyncSession = Depends(get_db),
_admin: User = Depends(get_current_admin),
) -> dict:
# Total user count (exclude admin accounts)
user_count = await session.scalar(
select(func.count(User.id)).where(User.role == 'user')
)
# Total platform storage in use
total_storage = await session.scalar(
select(func.sum(Quota.used_bytes))
) or 0
# Document status breakdown
status_rows = (await session.execute(
select(Document.status, func.count(Document.id))
.group_by(Document.status)
)).all()
doc_status = {row[0]: row[1] for row in status_rows}
# Last 10 audit entries (reuse existing query builder from audit.py)
audit_q = _build_filtered_query_with_handles(None, None, None, None).limit(10)
audit_rows = (await session.execute(audit_q)).all()
audit_items = [
_audit_to_dict_with_handles(row[0], row[1], row[2], row[3])
for row in audit_rows
]
return {
"user_count": user_count or 0,
"total_storage_bytes": total_storage,
"doc_status": doc_status,
"recent_audit": audit_items,
}
Important: _build_filtered_query_with_handles and _audit_to_dict_with_handles live in backend/api/audit.py which has router = APIRouter(prefix="/api/admin", ...). The import is a cross-module import from a sibling api/ module — valid in Python, no circular risk since overview.py does not import from admin/__init__.py. [VERIFIED: audit.py read directly for function signatures]
__init__.py update:
# backend/api/admin/__init__.py — add one import + one include_router call
from api.admin.overview import router as overview_router
router.include_router(overview_router)
Pattern 7: Tailwind safelist — exact color families from formatters.js
frontend/src/utils/formatters.js was read directly. The dynamic classes it generates: [VERIFIED: live codebase read]
providerColor returns: text-blue-500, text-sky-500, text-orange-500, text-gray-500, text-gray-400
providerBg returns: bg-blue-50, bg-sky-50, bg-orange-50, bg-gray-50
These use sky and orange color families that are NOT in D-14's proposed pattern (/bg-(blue|green|purple|orange|gray|indigo|red)/). sky is missing from D-14's pattern. The pattern must be expanded:
// frontend/tailwind.config.js — corrected safelist (D-14 adjustment)
safelist: [
{ pattern: /bg-(blue|sky|green|purple|orange|gray|indigo|red)-(50|100|500|600)/ },
{ pattern: /text-(blue|sky|green|purple|orange|gray|indigo|red)-(400|500|600|700)/ },
]
The sky family is needed for OneDrive (text-sky-500, bg-sky-50). The text-gray-400 fallback requires adding 400 to the text pattern.
Additionally, AuditLogTab.vue's actionTypeClass() function generates dynamic classes: bg-blue-50 text-blue-600, bg-gray-100 text-gray-600, bg-purple-50 text-purple-600, bg-amber-50 text-amber-700. The amber color family is also missing from D-14. The full corrected safelist:
safelist: [
{ pattern: /bg-(blue|sky|green|purple|orange|amber|gray|indigo|red)-(50|100|500|600)/ },
{ pattern: /text-(blue|sky|green|purple|orange|amber|gray|indigo|red)-(400|500|600|700)/ },
]
AdminUsersTab.vue also uses hardcoded complete class strings (bg-indigo-100 text-indigo-700, bg-green-100 text-green-700, bg-gray-100 text-gray-600) — these are complete literals in the template and will be found by Tailwind's scanner even without safelist. But providerColor/providerBg and actionTypeClass() both use dynamic construction and MUST be in the safelist.
Anti-Patterns to Avoid
- Adding a third v-else-if branch in App.vue for
route.meta.layout === 'admin': This causes double rendering.AdminLayoutis the route component —App.vue's<router-view />renders it. No App.vue changes needed. (PITFALLS.md §Pitfall 4) - Using
to.meta.requiresAdminfor child route guard: Only works on the parent. Useto.matched.some(r => r.meta.requiresAdmin). (PITFALLS.md §Pitfall 3) - Adding prefix to
overview.py'sAPIRouter(): Sub-routers have NO prefix. Parent__init__.pycarries/api/admin. (STATE.md §Key Decisions,__init__.pyconstraint comment) - Importing from
api.admin.__init__insideoverview.py: The__init__.pyimports from sub-modules. Sub-modules must not import from__init__.py— circular import. Import_build_filtered_query_with_handlesfromapi.auditdirectly. - Adding component-level auth checks in admin views: PITFALLS.md §Integration Pitfall C — the
beforeEachguard is the single authority. Components readauthStore.user(already populated by guard) and never callrefresh()themselves. - Keeping
AdminView.vueas a placeholder while building: D-12 says delete it after extraction. If it stays, it will be imported and confuse the router.
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| Admin layout switching | Third v-else-if in App.vue | Register AdminLayout as /admin route component |
Vue Router 4 nested route + router-view pattern handles this natively |
| Meta inheritance for child routes | Copy requiresAdmin: true to every child route |
to.matched.some(r => r.meta.requiresAdmin) |
Vue Router 4 matched array traversal is the designed API for this |
| Dynamic class purge prevention | Object with all complete class strings | Tailwind safelist with regex pattern |
Safelist is the official Tailwind v3 mechanism; object approach still has maintenance burden |
| Audit entries for overview | New query + new serializer | Import _build_filtered_query_with_handles + _audit_to_dict_with_handles from api/audit.py |
Already tested, handles edge cases, security-audited |
Runtime State Inventory
Not applicable — Phase 9 is a frontend rearchitecture + new backend endpoint. No renames, no data migration, no stored state is changed.
Common Pitfalls
Pitfall 1: Guard race on admin child route — to.meta.requiresAdmin is falsy for children
What goes wrong: After adding children to /admin, to.meta reflects only the deepest matched route's own meta. Children don't inherit requiresAdmin: true unless explicitly set.
Why it happens: Vue Router 4 does not merge parent meta into child meta by default.
How to avoid: Use to.matched.some(r => r.meta.requiresAdmin) — the only correct idiom.
Warning signs: Non-admin user can navigate directly to /admin/users in the browser after the rearchitecture.
Pitfall 2: Double layout — AdminLayout renders inside App.vue's user shell
What goes wrong: Temptation to add v-else-if="route.meta.layout === 'admin'" in App.vue, rendering AdminLayout as a component. But App.vue's <router-view /> has already resolved the route — the inner <router-view /> inside AdminLayout gets nothing.
Why it happens: Confusion between "layout as route component" vs "layout as App.vue branch".
How to avoid: AdminLayout is the /admin route's component: value — not a branch in App.vue. App.vue renders whatever the router resolves. The route resolves to AdminLayout, whose inner <router-view /> renders children.
Warning signs: Admin page renders with no sidebar, or admin sidebar renders with empty content.
Pitfall 3: Admin redirect loop on page reload
What goes wrong: Admin user reloads any /admin/* page. Token is gone (memory-only). Guard calls authStore.refresh(). If the D-09 admin→non-admin redirect runs before the refresh resolves, authStore.user is null, isAdmin is false, and the guard tries to redirect to /admin (itself), causing a loop.
Why it happens: D-09 check runs on the same to that triggered the guard. After refresh, user is populated — but the check order matters.
How to avoid: The guard structure shown in Pattern 2 is safe: refresh is awaited first, then both guard checks run with the populated authStore.user. The D-09 check !isAdminRoute && !to.meta.public && isAdmin will NOT trigger on /admin/* routes because isAdminRoute is true for those. No redirect loop.
Warning signs: Admins bounced to /login on page reload rather than staying at their admin page.
Pitfall 4: sky color family purged in production build
What goes wrong: D-14's proposed safelist pattern only covers blue|green|purple|orange|gray|indigo|red. OneDrive uses text-sky-500 and bg-sky-50. Audit log badge uses amber. Neither is in D-14's pattern. Production build purges them.
Why it happens: D-14 was drafted without consulting formatters.js and AuditLogTab.vue for the full color inventory.
How to avoid: Use the expanded pattern from Pattern 7 above that adds sky and amber.
Warning signs: OneDrive provider badge renders with default gray color in production. Audit action type badges lose color.
Pitfall 5: overview.py imports _build_filtered_query_with_handles from wrong place
What goes wrong: Developer imports from api.admin.audit (which doesn't exist) or from api.admin.__init__ (circular). The function lives in api.audit (the audit log module at backend/api/audit.py, NOT inside api/admin/).
Why it happens: Naming confusion — api/audit.py is a top-level module, not inside api/admin/. Its router carries prefix="/api/admin" but it lives at backend/api/audit.py.
How to avoid: Import: from api.audit import _build_filtered_query_with_handles, _audit_to_dict_with_handles
Pitfall 6: Comment purge removes the __init__.py constraint comment
What goes wrong: The comment in api/admin/__init__.py explaining "sub-routers carry NO prefix" is a WHY comment (constraint note) — it must be kept. CODE-09 purge criterion (D-16) says keep "why" comments. A mechanical purge might remove it.
Why it happens: The docstring at the top of __init__.py describes the constraint. It reads like a docstring (WHAT) but it is actually a constraint/pitfall note (WHY).
How to avoid: Evaluate each comment against D-16 criterion. The "NO prefix" note is a non-obvious invariant — keep it. Generic "Returns user list" docstrings in users.py — remove them.
Code Examples
AdminLayout.vue (complete)
<!-- Source: mirrors AuthLayout.vue structure [VERIFIED: live codebase read] -->
<template>
<div class="flex h-screen overflow-hidden">
<AdminSidebar />
<main class="flex-1 overflow-y-auto">
<div class="p-8 max-w-5xl mx-auto">
<router-view />
</div>
</main>
</div>
</template>
<script setup>
import AdminSidebar from '../components/admin/AdminSidebar.vue'
</script>
AdminSidebar.vue skeleton
<!-- Source: AppSidebar.vue CSS classes [VERIFIED: live codebase read] -->
<template>
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full shrink-0">
<div class="px-6 py-5 border-b border-gray-100">
<h1 class="text-lg font-bold text-indigo-600 tracking-tight">DocuVault</h1>
<p class="text-xs text-indigo-500 font-semibold mt-0.5">Admin</p>
</div>
<nav class="flex-1 px-3 py-4 overflow-y-auto">
<router-link to="/admin" class="nav-link"
:class="{ 'nav-link-active': $route.path === '/admin' }">
<!-- Overview SVG icon + "Overview" -->
</router-link>
<!-- Users, Quotas, AI Config, Audit Log nav links -->
</nav>
<!-- User identity footer (replicate from AppSidebar) -->
<div class="px-3 py-4 border-t border-gray-100">
<div v-if="authStore.user" class="flex items-center gap-3 px-4 py-3 border-t border-gray-100 -mx-3">
<!-- avatar + email + sign-out button -->
</div>
</div>
</aside>
</template>
<style scoped>
.nav-link { @apply flex items-center px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-100 hover:text-gray-900 transition-colors text-sm font-medium; }
.nav-link-active { @apply bg-indigo-50 text-indigo-700; }
</style>
Router /admin nested route (complete structure)
// Source: CONTEXT.md + PITFALLS.md §Pitfall 4 [VERIFIED: current router/index.js read]
// Replace the existing flat '/admin' route:
// { path: '/admin', component: () => import('../views/AdminView.vue'), meta: { requiresAdmin: true } }
// With:
{
path: '/admin',
component: () => import('../layouts/AdminLayout.vue'),
meta: { requiresAdmin: true },
children: [
{ path: '', component: () => import('../views/admin/AdminOverviewView.vue') },
{ 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') },
],
},
All admin views use lazy () => import(...) — consistent with existing auth views pattern and PERF-03 requirement in Phase 11.
Tailwind safelist (corrected from D-14)
// frontend/tailwind.config.js — add safelist property
// Source: formatters.js + AuditLogTab.vue direct reads [VERIFIED]
import forms from '@tailwindcss/forms'
export default {
content: ['./index.html', './src/**/*.{vue,js}'],
safelist: [
{ pattern: /bg-(blue|sky|green|purple|orange|amber|gray|indigo|red)-(50|100|500|600)/ },
{ pattern: /text-(blue|sky|green|purple|orange|amber|gray|indigo|red)-(400|500|600|700)/ },
],
theme: { extend: {} },
plugins: [forms],
}
backend/api/admin/__init__.py update
# Source: backend/api/admin/__init__.py [VERIFIED: live codebase read]
from api.admin.overview import router as overview_router
# Add alongside existing imports, then:
router.include_router(overview_router)
State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|---|---|---|---|
| Tab-based admin panel on user layout | Nested route subtree with dedicated layout | Phase 9 | Deep-linkable URLs, browser back button works |
to.meta.requiresAdmin guard |
to.matched.some(r => r.meta.requiresAdmin) |
Phase 9 | Security: guard now covers all child routes |
Flat /admin route |
/admin parent + child routes under AdminLayout |
Phase 9 | Standard Vue Router 4 layout nesting pattern |
All admins land at / then click Admin link |
Admin users redirected to /admin after login |
Phase 9 | Correct UX for operator-only accounts |
Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|---|---|---|
| A1 | SVG icon path strings for admin sidebar nav items (Overview=home, Users=group, Quotas=chart, AI=computer, Audit=clipboard) | Architecture Patterns §Pattern 5 | Wrong icon shown — cosmetic only, easy to fix |
| A2 | Importing _build_filtered_query_with_handles from api.audit inside overview.py does not create a circular import |
Architecture Patterns §Pattern 6 | ImportError at startup if circular; verify by checking api.audit imports |
All other claims in this research were verified by direct codebase read.
Open Questions (RESOLVED)
-
AdminSidebar — separate component or inline in AdminLayout?
- What we know:
AppSidebar.vueis a separate component imported byApp.vue.AuthLayout.vuehas no sidebar. - Recommendation: Create
AdminSidebar.vueas a separate component incomponents/admin/for consistency with the existing architecture pattern.AdminLayout.vueimports it.
- What we know:
-
AdminOverviewView.vue— does it need a Pinia store?- What we know: All other admin tab components (
AdminUsersTab, etc.) fetch data directly withapi.*calls inonMounted. D-03 says overview is a single-fetch endpoint. No coordination logic needed. - Recommendation: No store. Fetch directly in
onMountedwith localrefstate, consistent with all other admin tab components.
- What we know: All other admin tab components (
-
Comment purge scope for Phase 8 backend sub-packages — which files?
- What we know:
api/admin/users.py,api/admin/quotas.py,api/admin/ai.py,api/documents/upload.py,api/documents/crud.py,api/documents/content.py,api/auth/tokens.py,api/auth/totp.py,api/auth/password.py,api/auth/sessions.pyare all Phase 8 products. - Recommendation: The comment purge plan should enumerate all Phase 8 backend sub-package files explicitly so nothing is missed.
- What we know:
Environment Availability
Step 2.6: SKIPPED — Phase 9 is purely frontend restructuring + one new backend endpoint. No new external tools, services, or CLIs are required. All dependencies are already installed and running.
Validation Architecture
Test Framework
| Property | Value |
|---|---|
| Framework | pytest (backend) |
| Config file | backend/pytest.ini or backend/pyproject.toml |
| Quick run command | cd backend && pytest tests/test_admin_overview.py -x |
| Full suite command | cd backend && pytest -v |
Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|---|---|---|---|---|
| ADMIN-12 | to.matched.some() guard — non-admin cannot access /admin/users directly |
Integration (frontend router) | Manual browser test or Playwright | No — Wave 0 gap |
| ADMIN-11 | GET /api/admin/overview returns correct aggregate stats |
Integration (backend) | pytest tests/test_admin_overview.py -x |
No — Wave 0 gap |
| ADMIN-11 | Overview response never includes credentials_enc or document content |
Security test | pytest tests/test_admin_overview.py::test_overview_no_sensitive_fields |
No — Wave 0 gap |
| ADMIN-08 | AdminView.vue is deleted — no file imports it |
Static verification | grep -r "AdminView" frontend/src/ returns nothing |
N/A — verified by grep |
| CODE-06 | Dynamic color classes render in production build | Build test | npm run build then visual check |
N/A — manual |
Sampling Rate
- Per task commit:
cd backend && pytest tests/test_admin_overview.py -x(after overview endpoint) - Per wave merge:
cd backend && pytest -v - Phase gate: Full suite green before
/gsd:verify-work
Wave 0 Gaps
backend/tests/test_admin_overview.py— covers ADMIN-11 aggregate query + security invariants- Consider:
frontend/src/views/__tests__/AdminLayout.spec.jsif frontend test infra supports it (checkfrontend/src/views/__tests__/existence)
Security Domain
Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---|---|---|
| V2 Authentication | Partial | Guard + role check via authStore.user.role |
| V3 Session Management | No | Existing session management unchanged |
| V4 Access Control | Yes | requiresAdmin guard on all /admin/* children via to.matched.some() |
| V5 Input Validation | No | GET /api/admin/overview has no user-supplied input |
| V6 Cryptography | No | No crypto operations in this phase |
Known Threat Patterns
| Pattern | STRIDE | Standard Mitigation |
|---|---|---|
Non-admin accessing /admin/users directly via URL |
Elevation of Privilege | to.matched.some(r => r.meta.requiresAdmin) + get_current_admin dep on backend |
Admin redirect bypass via ?redirect=/ query param |
Privilege Escalation | D-08 login redirect checks user.role first; route.query.redirect honored only for same-role routes |
GET /api/admin/overview returning sensitive data |
Information Disclosure | Whitelist response fields; never return password_hash, credentials_enc, extracted_text; use same _audit_to_dict_with_handles whitelist for audit entries |
Admin endpoint invariant (from CLAUDE.md): GET /api/admin/overview must never include document content, extracted text, or credentials_enc. The overview response contains only aggregate counts and audit log summary rows — no document-level data. [VERIFIED by response design in Pattern 6]
Sources
Primary (HIGH confidence)
frontend/src/router/index.js— current guard structure, existing/adminroute definition, all route meta patternsfrontend/src/App.vue— current layout switch pattern (2 branches: auth/main)frontend/src/layouts/AuthLayout.vue— reference pattern for standalone layout componentfrontend/src/components/layout/AppSidebar.vue— CSS classes, SVG icon family, nav-link/nav-link-active, user footer structurefrontend/src/views/AdminView.vue— current tab structure being replacedfrontend/src/components/admin/AdminUsersTab.vue— no top-level padding confirmedfrontend/src/components/admin/AdminQuotasTab.vue— no top-level padding confirmedfrontend/src/components/admin/AuditLogTab.vue—actionTypeClass()amber/purple/blue color usagefrontend/src/utils/formatters.js— exact dynamic classes:text-sky-500,bg-sky-50,text-orange-500,text-gray-400,bg-orange-50frontend/tailwind.config.js— current config (no safelist, forms plugin already wired)frontend/src/stores/auth.js—user.value = data.userset on login,user.rolefield existsfrontend/src/views/auth/LoginView.vue—handleLoginResultredirect logicbackend/api/admin/__init__.py— aggregator pattern,router = APIRouter(prefix="/api/admin")backend/api/admin/users.py—router = APIRouter()with NO prefix confirmedbackend/api/admin/shared.py—_user_to_dictwhitelist patternbackend/api/audit.py—_build_filtered_query_with_handles,_audit_to_dict_with_handles— importable query helpersbackend/db/models.py—User,Quota,DocumentORM models for aggregate query.planning/research/PITFALLS.md— Pitfall 3 (meta guard), Pitfall 4 (App.vue double render), Pitfall 9 (double padding), Pitfall 14 (Tailwind safelist).planning/codebase/ARCHITECTURE.md— component responsibility map, layering.planning/phases/09-admin-panel-rearchitecture/09-CONTEXT.md— all locked decisions D-01 through D-16
Secondary (MEDIUM confidence)
- CONTEXT.md §Established Patterns — confirms
api/admin/__init__.py"sub-routers carry NO prefix" invariant
Tertiary (LOW confidence)
- SVG icon path strings for admin sidebar (A1 in Assumptions Log) — consistent with Heroicons outline family used in codebase but exact paths not individually verified against heroicons.com
Metadata
Confidence breakdown:
- Standard stack: HIGH — Phase 9 installs nothing new; all existing packages verified by direct file reads
- Architecture: HIGH — all patterns derived from live codebase reads + PITFALLS.md
- Pitfalls: HIGH — grounded in actual source files as annotated in PITFALLS.md header
- Tailwind safelist: HIGH —
formatters.jsandAuditLogTab.vueread directly; color families catalogued precisely
Research date: 2026-06-12 Valid until: 2026-07-12 (stable codebase — no external dependencies change)