Files
kite/.planning/milestones/v0.2-phases/09-admin-panel-rearchitecture/09-RESEARCH.md
T
curo1305andClaude Sonnet 4.6 123ae5b29b chore: archive v0.2 phase directories to milestones/v0.2-phases/
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>
2026-06-17 14:34:52 +02:00

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.py sub-module in backend/api/admin/. Single GET /api/admin/overview endpoint, one payload with all stats.
  • D-03: Audit entries inline in the overview response — no separate /api/audit call 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 /admin instead of /.
  • D-09: beforeEach guard: 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 use to.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.vue files and AdminView.vue deleted after extraction.
  • D-13: AuditLogTab.vue promoted as-is. No new features for the audit view in Phase 9.
  • D-14: Tailwind safelist pattern:
    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)/ },
    ]
    
    Researcher verifies which color families formatters.js actually 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)
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-100 with h1.text-lg.font-bold.text-indigo-600.tracking-tight + p.text-xs.text-gray-400.mt-0.5 for 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. AdminLayout is the route component — App.vue's <router-view /> renders it. No App.vue changes needed. (PITFALLS.md §Pitfall 4)
  • Using to.meta.requiresAdmin for child route guard: Only works on the parent. Use to.matched.some(r => r.meta.requiresAdmin). (PITFALLS.md §Pitfall 3)
  • Adding prefix to overview.py's APIRouter(): Sub-routers have NO prefix. Parent __init__.py carries /api/admin. (STATE.md §Key Decisions, __init__.py constraint comment)
  • Importing from api.admin.__init__ inside overview.py: The __init__.py imports from sub-modules. Sub-modules must not import from __init__.py — circular import. Import _build_filtered_query_with_handles from api.audit directly.
  • Adding component-level auth checks in admin views: PITFALLS.md §Integration Pitfall C — the beforeEach guard is the single authority. Components read authStore.user (already populated by guard) and never call refresh() themselves.
  • Keeping AdminView.vue as 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)

  1. AdminSidebar — separate component or inline in AdminLayout?

    • What we know: AppSidebar.vue is a separate component imported by App.vue. AuthLayout.vue has no sidebar.
    • Recommendation: Create AdminSidebar.vue as a separate component in components/admin/ for consistency with the existing architecture pattern. AdminLayout.vue imports it.
  2. AdminOverviewView.vue — does it need a Pinia store?

    • What we know: All other admin tab components (AdminUsersTab, etc.) fetch data directly with api.* calls in onMounted. D-03 says overview is a single-fetch endpoint. No coordination logic needed.
    • Recommendation: No store. Fetch directly in onMounted with local ref state, consistent with all other admin tab components.
  3. 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.py are all Phase 8 products.
    • Recommendation: The comment purge plan should enumerate all Phase 8 backend sub-package files explicitly so nothing is missed.

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.js if frontend test infra supports it (check frontend/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 /admin route definition, all route meta patterns
  • frontend/src/App.vue — current layout switch pattern (2 branches: auth/main)
  • frontend/src/layouts/AuthLayout.vue — reference pattern for standalone layout component
  • frontend/src/components/layout/AppSidebar.vue — CSS classes, SVG icon family, nav-link/nav-link-active, user footer structure
  • frontend/src/views/AdminView.vue — current tab structure being replaced
  • frontend/src/components/admin/AdminUsersTab.vue — no top-level padding confirmed
  • frontend/src/components/admin/AdminQuotasTab.vue — no top-level padding confirmed
  • frontend/src/components/admin/AuditLogTab.vueactionTypeClass() amber/purple/blue color usage
  • frontend/src/utils/formatters.js — exact dynamic classes: text-sky-500, bg-sky-50, text-orange-500, text-gray-400, bg-orange-50
  • frontend/tailwind.config.js — current config (no safelist, forms plugin already wired)
  • frontend/src/stores/auth.jsuser.value = data.user set on login, user.role field exists
  • frontend/src/views/auth/LoginView.vuehandleLoginResult redirect logic
  • backend/api/admin/__init__.py — aggregator pattern, router = APIRouter(prefix="/api/admin")
  • backend/api/admin/users.pyrouter = APIRouter() with NO prefix confirmed
  • backend/api/admin/shared.py_user_to_dict whitelist pattern
  • backend/api/audit.py_build_filtered_query_with_handles, _audit_to_dict_with_handles — importable query helpers
  • backend/db/models.pyUser, Quota, Document ORM 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.js and AuditLogTab.vue read directly; color families catalogued precisely

Research date: 2026-06-12 Valid until: 2026-07-12 (stable codebase — no external dependencies change)