--- phase: 02-users-authentication plan: 05 type: execute wave: 5 depends_on: - 02-02 - 02-03 - 02-04 files_modified: - frontend/src/views/AdminView.vue - frontend/src/components/admin/AdminUsersTab.vue - frontend/src/components/admin/AdminQuotasTab.vue - frontend/src/components/admin/AdminAiConfigTab.vue - frontend/src/components/layout/AppSidebar.vue autonomous: false requirements: - ADMIN-01 - ADMIN-02 - ADMIN-03 - ADMIN-04 - ADMIN-05 - ADMIN-07 must_haves: truths: - "Admin panel is accessible at /admin and visible in sidebar only for role='admin' users" - "Admin can view all users in a table with email, role, status, and action links" - "Admin can create a user with email, temporary password, and role via an inline form above the table" - "Admin can deactivate a user account with inline confirmation showing the user email" - "Admin can reset a user password, sending an email via the backend" - "Admin can edit a user's storage quota inline with a warning when new limit is below current usage" - "Admin can assign AI provider and model per user from dropdown selectors" - "Admin panel link in sidebar is absent for non-admin users" - "No impersonation action or UI exists anywhere" artifacts: - path: "frontend/src/views/AdminView.vue" provides: "Admin panel with horizontal tab strip: Users | Quotas | AI Config" - path: "frontend/src/components/admin/AdminUsersTab.vue" provides: "User table with create form, deactivate/reactivate/reset-password actions" - path: "frontend/src/components/admin/AdminQuotasTab.vue" provides: "Quota inline-edit table with usage warning" - path: "frontend/src/components/admin/AdminAiConfigTab.vue" provides: "AI provider/model dropdown per user with save" - path: "frontend/src/components/layout/AppSidebar.vue" provides: "Admin nav link (conditional) + user identity footer with sign-out button" key_links: - from: "frontend/src/views/AdminView.vue" to: "frontend/src/components/admin/AdminUsersTab.vue" via: "v-if on activeTab" pattern: "AdminUsersTab" - from: "frontend/src/components/layout/AppSidebar.vue" to: "useAuthStore().user?.role" via: "v-if conditional admin link" pattern: "role.*admin" --- Deliver the complete admin panel frontend. After this plan, an admin user can manage all user accounts, quotas, and AI configurations from the web UI. The sidebar shows the admin link only for admin users and the user identity footer enables sign-out. Purpose: This is the final wave plan — it wires all the admin API endpoints (Plan 04) into working Vue components with the exact UI-SPEC visual contract. Output: AdminView.vue, three admin tab components, AppSidebar.vue update with admin link and user footer. @/Users/nik/.claude/get-shit-done/workflows/execute-plan.md @/Users/nik/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/02-users-authentication/02-CONTEXT.md @.planning/phases/02-users-authentication/02-PATTERNS.md @.planning/phases/02-users-authentication/02-UI-SPEC.md @.planning/phases/02-users-authentication/02-04-SUMMARY.md From frontend/src/api/client.js (Plan 02 exports): adminListUsers() → GET /api/admin/users → { items: [{ id, handle, email, role, is_active, totp_enabled, ai_provider, ai_model, created_at }] } adminCreateUser(body) → POST /api/admin/users → 201 { id, handle, email, role, created_at } adminDeactivateUser(id) → PATCH /api/admin/users/{id}/status { is_active: false } adminReactivateUser(id) → PATCH /api/admin/users/{id}/status { is_active: true } adminResetUserPassword(id) → POST /api/admin/users/{id}/password-reset → 202 adminUpdateQuota(id, limitBytes) → PATCH /api/admin/users/{id}/quota { limit_bytes: limitBytes } adminUpdateAiConfig(id, provider, model) → PATCH /api/admin/users/{id}/ai-config { ai_provider, ai_model } From frontend/src/stores/auth.js (Plan 02): const user = ref(null) // { id, handle, email, role, totp_enabled } function logout() // clears accessToken and user From frontend/src/components/layout/AppSidebar.vue (current — must be extended, not recreated): UI-SPEC admin panel visual contract: Tab strip: flex border-b border-gray-200 mb-6 Active tab: px-4 py-2 text-sm font-semibold text-indigo-600 border-b-2 border-indigo-600 Inactive tab: px-4 py-2 text-sm font-semibold text-gray-500 hover:text-gray-700 border-b-2 border-transparent Table: bg-white rounded-xl border border-gray-200 overflow-hidden divide-y divide-gray-200 Deactivate link: text-red-600 hover:text-red-700 text-sm Other actions: text-indigo-600 hover:text-indigo-700 text-sm Create button: bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold px-4 py-2 rounded-lg After Plans 02, 03, and 04 complete, the full auth and admin backend is live. This checkpoint verifies the backend before building the admin frontend against it. 1. `docker compose up` — confirm all services start cleanly 2. `curl -s http://localhost:8000/health` → { status: "ok", ... } 3. Register a user: `curl -s -X POST http://localhost:8000/api/auth/register -H 'Content-Type: application/json' -d '{"handle":"testuser","email":"test@test.com","password":"TestPass12!"}'` → 201 4. Login: `curl -s -X POST http://localhost:8000/api/auth/login -H 'Content-Type: application/json' -d '{"email":"test@test.com","password":"TestPass12!"}' -v` → 200 + Set-Cookie header containing "HttpOnly" and "SameSite=Strict" 5. Try GET /api/admin/users without admin token → 403 6. Check docker compose logs for admin bootstrap: should see "Admin bootstrap" log line or "ADMIN_EMAIL not set" warning Type "backend verified" or describe any issues found Task 2: Admin tab components and AdminView frontend/src/views/AdminView.vue, frontend/src/components/admin/AdminUsersTab.vue, frontend/src/components/admin/AdminQuotasTab.vue, frontend/src/components/admin/AdminAiConfigTab.vue - frontend/src/views/SettingsView.vue (tabbed sections pattern — v-if on activeTab, button tab strip, card layout) - frontend/src/views/TopicsView.vue (list + action pattern for AdminUsersTab) - .planning/phases/02-users-authentication/02-UI-SPEC.md (Admin View section — table structure, tab strip classes, row states, create user panel, quota inline edit, AI config table) - .planning/phases/02-users-authentication/02-PATTERNS.md (AdminUsersTab, AdminQuotasTab, AdminAiConfigTab, AdminView sections) - .planning/phases/02-users-authentication/02-UI-SPEC.md (Copywriting Contract — admin entries, loading states) frontend/src/views/AdminView.vue: Heading "Admin panel" (text-2xl font-semibold text-gray-900). Horizontal tab strip using UI-SPEC tab classes. Tabs: "Users" | "Quotas" | "AI Config". v-if switch: AdminUsersTab, AdminQuotasTab, AdminAiConfigTab. Import all three components. No guard needed here — /admin route is protected by router guard; AdminView only renders when admin user is authenticated. frontend/src/components/admin/AdminUsersTab.vue: On mounted, call adminListUsers() and store in users ref. Table columns per UI-SPEC: Email | Role | Status | Created | Actions. Role badge: same indigo/gray badge styles as AccountView. Status badge: active=bg-green-100 text-green-700, deactivated=bg-gray-100 text-gray-500. Actions column: for active rows "Reset password" · "Deactivate"; for deactivated rows "Reactivate". Action link classes per UI-SPEC. Deactivate flow: clicking "Deactivate" replaces the row's action cell with an inline confirmation showing the user email ("Deactivate [email]? They will lose access immediately. Their data is preserved." + "Deactivate" / "Keep account" buttons). On confirm: call adminDeactivateUser(id), update row is_active=false. No modal — inline replacement per UI-SPEC. Create user: "Create user" button top-right of table. On click: show inline panel above table (not a modal) with: email input, role selector (User/Admin dropdown), password field (pre-generated 12-char random password, read-only, with copy button) — executor should generate the temp password client-side via crypto.getRandomValues to produce a strong alphanumeric string. On submit: adminCreateUser({ handle: email.split('@')[0], email, password, role }) then prepend to users list. Copywriting: "Create user" CTA. Loading states: per UI-SPEC loading table — row-level spinner (animate-spin rounded-full border-2 border-current border-t-transparent w-4 h-4 inline) in the action column while deactivate/reset operations are in flight; pointer-events-none on the row. Empty state: if users.length === 0 after load: heading "No users yet" + body "Create the first user account to get started." — center in table area. frontend/src/components/admin/AdminQuotasTab.vue: On mounted, call adminListUsers(). Table columns: Email | Used | Limit | Usage % | Actions. Used and Limit displayed in MB (Math.round(bytes / 1048576)) + " MB" suffix. Usage %: Math.round(used/limit * 100) + "%". Actions: "Edit" button per row. On "Edit": the Limit cell becomes an input type="number" min="1" step="1" (value in MB) + Save/Cancel buttons. On save: adminUpdateQuota(id, newMB * 1048576). If response.warning is true, show inline warning below input: text-xs text-amber-600 "New limit is below current usage (X MB). Existing documents will not be deleted, but uploads will be blocked." Still apply the update. Loading: spinner inline in save button + disabled. frontend/src/components/admin/AdminAiConfigTab.vue: On mounted, call adminListUsers(). Table columns: Email | AI Provider | AI Model | Actions. Provider: dropdown