# 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:
  ```js
  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)
```

### 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`.

```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:**
```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

```javascript
// 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):**
```javascript
// 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:**
```javascript
// 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:

```javascript
// 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>`):
  ```css
  .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]

```python
# 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:**

```python
@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:**
```python
# 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:

```javascript
// 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:

```javascript
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)

```vue
<!-- 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

```vue
<!-- 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)

```javascript
// 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)

```javascript
// 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

```python
# 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.vue` — `actionTypeClass()` 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.js` — `user.value = data.user` set on login, `user.role` field exists
- `frontend/src/views/auth/LoginView.vue` — `handleLoginResult` redirect logic
- `backend/api/admin/__init__.py` — aggregator pattern, `router = APIRouter(prefix="/api/admin")`
- `backend/api/admin/users.py` — `router = 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.py` — `User`, `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)
