---
phase: 09-admin-panel-rearchitecture
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
  - frontend/src/layouts/AdminLayout.vue
  - frontend/src/components/admin/AdminSidebar.vue
  - frontend/src/views/admin/AdminOverviewView.vue
  - frontend/src/api/admin.js
autonomous: true
requirements:
  - ADMIN-08
  - ADMIN-09
  - ADMIN-11

user_setup: []

must_haves:
  truths:
    - "AdminLayout.vue renders an aside (AdminSidebar) and a main content area containing a router-view"
    - "AdminSidebar shows the DocuVault logo with an 'Admin' subtitle and five nav links: Overview, Users, Quotas, AI Config, Audit Log — no Back to app link"
    - "AdminOverviewView fetches GET /api/admin/overview on mount and renders four stat cards + a 10-row audit table"
    - "frontend/src/api/admin.js exports getAdminOverview()"
  artifacts:
    - path: "frontend/src/layouts/AdminLayout.vue"
      provides: "Admin route shell (sidebar + router-view)"
      contains: "router-view"
    - path: "frontend/src/components/admin/AdminSidebar.vue"
      provides: "Admin-only sidebar nav with 5 router-links, no Back to app"
      contains: "to=\"/admin/audit\""
    - path: "frontend/src/views/admin/AdminOverviewView.vue"
      provides: "Stats cards + recent audit table at /admin"
      contains: "getAdminOverview"
    - path: "frontend/src/api/admin.js"
      provides: "getAdminOverview() API client function"
      contains: "export async function getAdminOverview"
  key_links:
    - from: "frontend/src/layouts/AdminLayout.vue"
      to: "frontend/src/components/admin/AdminSidebar.vue"
      via: "import AdminSidebar"
      pattern: "import AdminSidebar from '\\.\\./components/admin/AdminSidebar.vue'"
    - from: "frontend/src/views/admin/AdminOverviewView.vue"
      to: "frontend/src/api/admin.js"
      via: "getAdminOverview()"
      pattern: "getAdminOverview\\("
    - from: "frontend/src/api/admin.js"
      to: "/api/admin/overview"
      via: "request()"
      pattern: "/api/admin/overview"
---

<objective>
Create the admin route shell — `AdminLayout.vue` (the route component at `/admin`), `AdminSidebar.vue` (the admin-only sidebar with five nav links per D-07 and no Back-to-app per D-06), and `AdminOverviewView.vue` (the new overview view at `/admin` showing stats cards + last-10 audit table per D-01, D-03). Extend the frontend `api/admin.js` module with `getAdminOverview()` so the view can fetch the new backend endpoint.

Purpose: ADMIN-08 requires `AdminLayout.vue` as the `/admin` route component with its own sidebar; ADMIN-09 defines the nav set (D-06 overrides the Back-to-app link); ADMIN-11 requires the overview UI. This plan builds the shell + new view; routing is wired in 09-04.

Output: 4 new/modified frontend files. The shell renders correctly when manually mounted; the overview view single-fetches the backend endpoint built in 09-01.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/09-admin-panel-rearchitecture/09-CONTEXT.md
@.planning/phases/09-admin-panel-rearchitecture/09-RESEARCH.md
@.planning/phases/09-admin-panel-rearchitecture/09-PATTERNS.md
@CLAUDE.md
@frontend/src/layouts/AuthLayout.vue
@frontend/src/components/layout/AppSidebar.vue
@frontend/src/components/admin/AuditLogTab.vue
@frontend/src/api/admin.js
@frontend/src/stores/auth.js
@frontend/src/utils/formatters.js

<interfaces>
From frontend/src/api/client.js (re-export barrel from Phase 8):
  - `request(path, options)` — the canonical fetch helper with 401-refresh handling. New API helpers MUST call `request()` and not roll their own `fetch`. Import via `./utils.js` in the domain module: `import { request } from './utils.js'`.

From frontend/src/api/admin.js (existing domain module — Phase 8 CODE-04 output):
  - Already exports `listAdminUsers`, `createAdminUser`, `setAdminUserQuota`, `getAiConfig`, etc. via `request()`. Add `getAdminOverview` alongside these.

From frontend/src/stores/auth.js:
  - `useAuthStore()` exposes `user` (ref with `{ email, role, ... }`) and `logout()` action. AdminSidebar imports the store for the user footer + sign-out.

From frontend/src/components/layout/AppSidebar.vue (reference pattern):
  - Container: `<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full shrink-0">`
  - Logo block: `<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-gray-400 mt-0.5">Document Manager</p></div>`
  - Nav: `<nav class="flex-1 px-3 py-4 overflow-y-auto">…</nav>`
  - 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; }` and `.nav-link-active { @apply bg-indigo-50 text-indigo-700; }`
  - SVG attrs: `class="w-4 h-4 mr-2 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"` with `stroke-linecap="round" stroke-linejoin="round" stroke-width="2"`
  - User footer block exists at the bottom with avatar initial + email + sign-out button.

From frontend/src/layouts/AuthLayout.vue (reference pattern):
  - Simple template-only layout shell hosting `<router-view />`. AdminLayout follows the same "layout-as-route-component" pattern; App.vue requires NO changes.
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Add getAdminOverview() to api/admin.js</name>
  <files>frontend/src/api/admin.js</files>
  <read_first>
    - frontend/src/api/admin.js (current exports — Phase 8 CODE-04 file; find the existing `request` import and the pattern other GET helpers use, e.g. `listAdminUsers`)
    - frontend/src/api/utils.js (confirm `request` is exported here per Phase 8 CODE-04)
    - frontend/src/api/client.js (verify `getAdminOverview` re-export is automatic via the barrel)
  </read_first>
  <behavior>
    - `getAdminOverview()` returns a Promise resolving to `{ user_count, total_storage_bytes, doc_status, recent_audit }`.
    - On 401 the underlying `request()` triggers a silent refresh; on non-401 errors the rejected Promise carries the server message.
    - Function is exported as a named export and re-exported automatically from `client.js` via the existing barrel.
  </behavior>
  <action>Open `frontend/src/api/admin.js`. Add a new named export `getAdminOverview` that calls `request('/api/admin/overview')` and returns its result (no extra wrapping). Follow the exact style of the existing GET helpers in the same file (likely `export async function getAdminOverview() { return request('/api/admin/overview') }`). Do NOT define a duplicate `request` import; reuse whatever import line already exists in `admin.js`. Do NOT touch `client.js` — Phase 8 CODE-04 made it a re-export barrel that picks up every named export from `admin.js` automatically.</action>
  <verify>
    <automated>cd frontend && grep -c "export async function getAdminOverview" src/api/admin.js && grep -c "/api/admin/overview" src/api/admin.js</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c "export async function getAdminOverview" frontend/src/api/admin.js` returns 1.
    - `grep -c "'/api/admin/overview'" frontend/src/api/admin.js` returns 1.
    - No new `import` line for `request` is added; existing import is reused.
    - `cd frontend && node -e "import('./src/api/admin.js').then(m => { if (typeof m.getAdminOverview !== 'function') process.exit(1) })"` exits 0 (function is exported).
  </acceptance_criteria>
  <done>`getAdminOverview()` exists in `api/admin.js` and resolves via the standard `request()` helper.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Create AdminLayout.vue + AdminSidebar.vue</name>
  <files>frontend/src/layouts/AdminLayout.vue, frontend/src/components/admin/AdminSidebar.vue</files>
  <read_first>
    - frontend/src/layouts/AuthLayout.vue (template-only layout pattern; confirms the "no App.vue branch" idiom)
    - frontend/src/components/layout/AppSidebar.vue (CSS classes, scoped style block, SVG family, user footer, sign-out function — copy these verbatim)
    - frontend/src/stores/auth.js (signature of `useAuthStore()` and `logout()`)
    - PATTERNS.md §AdminSidebar.vue (SVG path strings for the five nav icons)
  </read_first>
  <behavior>
    - `AdminLayout.vue` template: `<div class="flex h-screen overflow-hidden">` with `<AdminSidebar />` then `<main class="flex-1 overflow-y-auto"><div class="p-8 max-w-5xl mx-auto"><router-view /></div></main>`. Uses `<script setup>` with `import AdminSidebar from '../components/admin/AdminSidebar.vue'`.
    - `AdminSidebar.vue` renders an `<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full shrink-0">` matching `AppSidebar.vue` chrome.
    - Header subtitle reads "Admin" with classes `text-xs text-indigo-500 font-semibold mt-0.5` (D-05) instead of "Document Manager".
    - Nav contains exactly 5 `<router-link>` entries in D-07 order: Overview (`/admin`), Users (`/admin/users`), Quotas (`/admin/quotas`), AI Config (`/admin/ai`), Audit Log (`/admin/audit`). No "Back to app" link (D-06).
    - Overview active-state check uses `$route.path === '/admin'`; the other four use `$route.path.startsWith('/admin/users')` etc.
    - Footer: avatar initial (`authStore.user.email[0].toUpperCase()` with `?` fallback) + email + sign-out button that calls `authStore.logout()` then `router.push('/login')`.
    - Scoped `<style scoped>` block contains `.nav-link` and `.nav-link-active` with the exact `@apply` lines from `AppSidebar.vue`.
  </behavior>
  <action>Create `frontend/src/layouts/AdminLayout.vue` mirroring `AuthLayout.vue` structure but with the flex-row layout from PATTERNS.md §AdminLayout (sidebar + main + p-8 max-w-5xl mx-auto content wrapper). The `<router-view />` lives inside the content wrapper. `<script setup>` imports `AdminSidebar` from `../components/admin/AdminSidebar.vue` only — no other imports needed. Create `frontend/src/components/admin/AdminSidebar.vue`. Template: `<aside>` container with the exact classes from `<interfaces>`, header `<div>` with `<h1>DocuVault</h1>` (same classes as `AppSidebar.vue`) and `<p class="text-xs text-indigo-500 font-semibold mt-0.5">Admin</p>` (D-05). `<nav class="flex-1 px-3 py-4 overflow-y-auto">` containing 5 `<router-link>` elements in D-07 order. Each link uses `class="nav-link"` with `:class="{ 'nav-link-active': … }"` and contains an inline `<svg>` icon (use the path strings from PATTERNS.md §AdminSidebar.vue table for Overview/Users/Quotas/AI Config/Audit Log) followed by the link label. The Overview link active-state uses `$route.path === '/admin'`; the others use `$route.path.startsWith('/admin/<segment>')`. SVG attributes: `class="w-4 h-4 mr-2 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"` and each `<path>` has `stroke-linecap="round" stroke-linejoin="round" stroke-width="2"`. Footer block at the bottom replicates the avatar+email+sign-out pattern from `AppSidebar.vue` — copy that block verbatim and ensure it ends INSIDE the `<aside>`. `<script setup>` imports: `import { useRouter } from 'vue-router'`, `import { useAuthStore } from '../../stores/auth.js'`; sets `const authStore = useAuthStore()`, `const router = useRouter()`, and defines `async function signOut() { await authStore.logout(); router.push('/login') }`. Scoped `<style scoped>` block contains the two `@apply` rules from `<interfaces>`. DO NOT add a "Back to app" router-link anywhere (D-06).</action>
  <verify>
    <automated>cd frontend && grep -c "router-view" src/layouts/AdminLayout.vue && grep -c "to=\"/admin/audit\"" src/components/admin/AdminSidebar.vue && grep -c "Back to app" src/components/admin/AdminSidebar.vue</automated>
  </verify>
  <acceptance_criteria>
    - `frontend/src/layouts/AdminLayout.vue` exists.
    - `grep -c "<router-view" frontend/src/layouts/AdminLayout.vue` returns 1.
    - `grep -c "import AdminSidebar from '../components/admin/AdminSidebar.vue'" frontend/src/layouts/AdminLayout.vue` returns 1.
    - `grep -c "p-8 max-w-5xl mx-auto" frontend/src/layouts/AdminLayout.vue` returns 1.
    - `frontend/src/components/admin/AdminSidebar.vue` exists.
    - `grep -c "to=\"/admin\"" frontend/src/components/admin/AdminSidebar.vue` returns at least 1 (Overview link).
    - `grep -c "to=\"/admin/users\"" frontend/src/components/admin/AdminSidebar.vue` returns 1.
    - `grep -c "to=\"/admin/quotas\"" frontend/src/components/admin/AdminSidebar.vue` returns 1.
    - `grep -c "to=\"/admin/ai\"" frontend/src/components/admin/AdminSidebar.vue` returns 1.
    - `grep -c "to=\"/admin/audit\"" frontend/src/components/admin/AdminSidebar.vue` returns 1.
    - `grep -ci "back to app" frontend/src/components/admin/AdminSidebar.vue` returns 0 (D-06).
    - `grep -c "text-indigo-500 font-semibold" frontend/src/components/admin/AdminSidebar.vue` returns at least 1 (Admin badge).
    - `grep -c "@apply" frontend/src/components/admin/AdminSidebar.vue` returns 2 (.nav-link + .nav-link-active).
    - `grep -c "authStore.logout" frontend/src/components/admin/AdminSidebar.vue` returns 1.
    - `cd frontend && npx vue-tsc --noEmit 2>&1 | grep -E "(AdminLayout|AdminSidebar)" | wc -l` returns 0 (no template/script errors in these two files); if `vue-tsc` is not configured, fall back to `npm run build` succeeding.
  </acceptance_criteria>
  <done>AdminLayout + AdminSidebar exist with the correct chrome; no Back-to-app link; nav links cover the five admin destinations in D-07 order.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 3: Create AdminOverviewView.vue</name>
  <files>frontend/src/views/admin/AdminOverviewView.vue</files>
  <read_first>
    - frontend/src/components/admin/AuditLogTab.vue (table header + row pattern for the recent audit table; copy the table column layout for `event_type`, `owner_handle`, `target_user_handle`, `created_at`, `ip_address`)
    - frontend/src/components/admin/AdminUsersTab.vue (data-fetch-on-mount pattern with local `ref` + `onMounted` + try/catch; loading + error UI block)
    - frontend/src/utils/formatters.js (use existing `formatSize` for `total_storage_bytes` and `formatDate` for audit `created_at`)
    - frontend/src/api/admin.js (confirm `getAdminOverview` export from Task 1)
  </read_first>
  <behavior>
    - Template root is a single `<div>` with NO top-level padding (AdminLayout owns `p-8 max-w-5xl mx-auto`).
    - Loading block ("Loading overview…") shown when `loading.value === true`.
    - Error block (red text) shown when `error.value` is non-null.
    - Stat cards section: a 4-column grid (`grid grid-cols-1 md:grid-cols-4 gap-4`) of four `<div class="bg-white border border-gray-200 rounded-xl p-6">` cards: Users (`overview.user_count`), Storage (`formatSize(overview.total_storage_bytes)`), Processing (`overview.doc_status.processing ?? 0`), Ready (`overview.doc_status.ready ?? 0`). A fifth "Failed" card may be added beside or under (D-01 says 3-4 cards; 4 is the chosen number — choice noted in summary).
    - Recent audit table renders `overview.recent_audit` (max 10 rows) with columns: When (`formatDate(entry.created_at)`), Event (`entry.event_type`), Actor (`entry.owner_handle ?? '—'`), Target (`entry.target_user_handle ?? '—'`), IP (`entry.ip_address ?? '—'`). Uses the same table CSS structure as `AuditLogTab.vue`.
    - Empty audit array renders "No recent activity" placeholder text — NOT a generic "No items" (a proper EmptyState component lands in Phase 10; for Phase 9 a one-line placeholder is acceptable).
  </behavior>
  <action>Create `frontend/src/views/admin/AdminOverviewView.vue`. Template root: `<div>`. Inside, top section is `<h2 class="text-xl font-semibold text-gray-900 mb-6">Overview</h2>`. Below that: loading conditional `<div v-if="loading" class="text-gray-500">Loading overview…</div>`, error conditional `<div v-else-if="error" class="text-red-600">{{ error }}</div>`, then content `<template v-else-if="overview">` containing the stat-cards grid then the recent audit table. Use exactly four stat cards (Users, Storage, Processing, Ready) — choice rationale: ADMIN-11 lists 4 stats explicitly; D-01 says 3-4 cards (4 chosen). The Processing/Ready cards read `overview.doc_status?.processing ?? 0` and `overview.doc_status?.ready ?? 0` respectively. The audit table column layout copies AuditLogTab.vue's table head and row structure for the five fields listed in `<behavior>`. `<script setup>`: `import { ref, onMounted } from 'vue'`; `import { getAdminOverview } from '../../api/admin.js'`; `import { formatSize, formatDate } from '../../utils/formatters.js'`; `const loading = ref(false)`, `const error = ref(null)`, `const overview = ref(null)`; `onMounted(async () => { loading.value = true; try { overview.value = await getAdminOverview() } catch (e) { error.value = e?.message || 'Failed to load overview' } finally { loading.value = false } })`. No Pinia store — single-fetch view (RESEARCH.md Open Question 2 — chose "no store"). No inline color classes that need safelist — formatSize/formatDate return strings, not class names.</action>
  <verify>
    <automated>cd frontend && grep -c "getAdminOverview" src/views/admin/AdminOverviewView.vue && grep -c "formatSize" src/views/admin/AdminOverviewView.vue && grep -c "recent_audit" src/views/admin/AdminOverviewView.vue</automated>
  </verify>
  <acceptance_criteria>
    - File `frontend/src/views/admin/AdminOverviewView.vue` exists.
    - First non-comment template element is `<div>` with no `p-8`, `p-6`, or `max-w-` class (AdminLayout owns the padding).
    - `grep -c "getAdminOverview" frontend/src/views/admin/AdminOverviewView.vue` returns at least 1 (import) and one call.
    - `grep -c "from '../../utils/formatters.js'" frontend/src/views/admin/AdminOverviewView.vue` returns 1.
    - `grep -c "doc_status" frontend/src/views/admin/AdminOverviewView.vue` returns at least 2 (processing + ready cards).
    - `grep -c "total_storage_bytes" frontend/src/views/admin/AdminOverviewView.vue` returns at least 1.
    - `grep -c "user_count" frontend/src/views/admin/AdminOverviewView.vue` returns at least 1.
    - `grep -c "recent_audit" frontend/src/views/admin/AdminOverviewView.vue` returns at least 1.
    - `grep -cE "p-8|max-w-5xl" frontend/src/views/admin/AdminOverviewView.vue` returns 0 (no double padding).
    - `cd frontend && npm run build` succeeds (the view compiles even though router doesn't reference it yet — Vite reports it as an unused module but no error).
  </acceptance_criteria>
  <done>AdminOverviewView fetches `/api/admin/overview` on mount and renders four stat cards + a recent-audit table; no top-level padding; no Pinia store.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Vue runtime → backend admin API | new `getAdminOverview()` call crosses; relies on shared `request()` for auth + refresh |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-09-02-01 | Information Disclosure | AdminOverviewView render | accept | Component only renders fields returned by the backend; backend whitelist (T-09-01-02) is the authoritative gate. No `v-html` or innerHTML; Vue auto-escaping handles XSS. |
| T-09-02-02 | Elevation of Privilege | AdminLayout shown to non-admin | mitigate | Router guard updated in 09-04 (`to.matched.some(r => r.meta.requiresAdmin)`) ensures the layout never mounts for non-admin users; this plan ships the chrome only. |
| T-09-02-03 | Spoofing (Sidebar logout) | `authStore.logout()` call | accept | Reuses existing logout flow audited in Phase 7.1; no new code path. |
</threat_model>

<verification>
- `cd frontend && npm run build` → succeeds.
- `grep -ci "back to app" frontend/src/components/admin/AdminSidebar.vue` → 0 (D-06).
- Five admin destinations covered by `<router-link to="...">` in the sidebar.
- `AdminOverviewView.vue` calls `getAdminOverview()` and uses `formatSize`/`formatDate` from shared formatters.
</verification>

<success_criteria>
- ADMIN-08: `AdminLayout.vue` exists as a standalone layout with its own sidebar and a `<router-view />`.
- ADMIN-09: AdminSidebar has the five D-07 nav items in order with SVG icons; no Back-to-app link (D-06 override applied).
- ADMIN-11: AdminOverviewView single-fetches `/api/admin/overview` and displays the four stats + last-10 audit table.
- `getAdminOverview()` lives in `api/admin.js`; the `client.js` barrel re-exports it without modification (Phase 8 CODE-04 invariant preserved).
</success_criteria>

<output>
Create `.planning/phases/09-admin-panel-rearchitecture/09-02-SUMMARY.md` when done. Include: files created, the 4-card choice rationale (vs 3), and confirmation that no Back-to-app link is present and no top-level padding is duplicated in AdminOverviewView.
</output>
