797 lines
42 KiB
Markdown
797 lines
42 KiB
Markdown
# 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)
|