# Settings Rework Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Replace the horizontal tab bar in SettingsView with a sidebar, apply Option-A grouped-card content style, inline Users/Logs views as sidebar panels, and convert the header cog from a dropdown to a direct link. **Architecture:** Three sequential tasks — (1) AppHeader + Router cleanup removes the dropdown and old routes, (2) SettingsView layout converts tab bar to sidebar with card-style panel headers, (3) SettingsView panels folds in UserManagement and Logs script/template logic as new Admin panels. **Tech Stack:** Vue 3, TypeScript, Vue Router, Pinia, scoped CSS — no new dependencies. --- ## Chunk 1: AppHeader + Router ### Task 1: Remove gear dropdown; cog becomes router-link **Files:** - Modify: `frontend/src/components/AppHeader.vue` - Modify: `frontend/src/router/index.ts` No automated tests exist for navigation — verify manually after each step. - [ ] **Step 1: Remove gear state/logic from AppHeader script** In `frontend/src/components/AppHeader.vue`, delete these lines from ``): ```typescript // ── Users panel ── import type { User } from "@/types/auth"; interface Invitation { id: number; email: string; created_at: string; expires_at: string; } const users = ref([]); const registrationOpen = ref(false); const usersLoading = ref(false); const toggling = ref(false); const confirmDeleteId = ref(null); const deleting = ref(null); const inviteEmail = ref(""); const sendingInvite = ref(false); const invitations = ref([]); const revokingId = ref(null); async function fetchUsers() { try { const data = await apiGet<{ users: User[] }>("/api/admin/users"); users.value = data.users; } catch { toastStore.show("Failed to load users", "error"); } } async function fetchRegistration() { try { const data = await apiGet<{ open: boolean }>("/api/admin/registration"); registrationOpen.value = data.open; } catch { /* ignore */ } } async function fetchInvitations() { try { const data = await apiGet<{ invitations: Invitation[] }>("/api/admin/invitations"); invitations.value = data.invitations; } catch { /* ignore */ } } async function loadUsersPanel() { if (users.value.length > 0) return; // already loaded usersLoading.value = true; await Promise.all([fetchUsers(), fetchRegistration(), fetchInvitations()]); usersLoading.value = false; } async function sendInvite() { const email = inviteEmail.value.trim().toLowerCase(); if (!email) return; sendingInvite.value = true; try { await apiPost("/api/admin/invitations", { email }); toastStore.show(`Invitation sent to ${email}`); inviteEmail.value = ""; await fetchInvitations(); } catch (e: unknown) { const body = (e as { body?: { error?: string } })?.body; toastStore.show(body?.error || "Failed to send invitation", "error"); } finally { sendingInvite.value = false; } } async function revokeInvitation(id: number) { revokingId.value = id; try { await apiDelete(`/api/admin/invitations/${id}`); invitations.value = invitations.value.filter((inv) => inv.id !== id); toastStore.show("Invitation revoked"); } catch { toastStore.show("Failed to revoke invitation", "error"); } finally { revokingId.value = null; } } async function toggleRegistration() { toggling.value = true; try { const data = await apiPut<{ open: boolean }>("/api/admin/registration", { open: !registrationOpen.value, }); registrationOpen.value = data.open; toastStore.show(data.open ? "Registration opened" : "Registration closed"); } catch { toastStore.show("Failed to update registration setting", "error"); } finally { toggling.value = false; } } function confirmDelete(userId: number) { if (confirmDeleteId.value === userId) { deleteUser(userId); } else { confirmDeleteId.value = userId; } } function cancelDelete() { confirmDeleteId.value = null; } async function deleteUser(userId: number) { confirmDeleteId.value = null; deleting.value = userId; try { await apiDelete(`/api/admin/users/${userId}`); users.value = users.value.filter((u) => u.id !== userId); toastStore.show("User deleted"); } catch (e: unknown) { const body = (e as { body?: { error?: string } })?.body; toastStore.show(body?.error || "Failed to delete user", "error"); } finally { deleting.value = null; } } function formatUserDate(iso: string): string { return new Date(iso).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", }); } ``` Note: `apiDelete` must be imported — add it to the existing import line: ```typescript // Change: import { apiGet, apiPost, apiPut } from "@/api/client"; // To: import { apiGet, apiPost, apiPut, apiDelete } from "@/api/client"; ``` Replace the existing `watch(activeTab, ...)` that only saves to localStorage (already updated in Task 2) so it now also handles lazy panel loading. The single consolidated watcher should be: ```typescript watch(activeTab, (v) => { localStorage.setItem("settings_tab", v === "admin" ? "config" : v); if (v === "users" && authStore.isAdmin) loadUsersPanel(); if (v === "logs" && authStore.isAdmin) loadLogsPanel(); }); ``` (The `v === "admin"` normalization is kept here as a safety net but should never fire after the migration in Task 2.) - [ ] **Step 2: Add Users panel template** In `frontend/src/views/SettingsView.vue` template, after the `` panel closing `` (currently at line 917) and before ``, add: ```html

Registration

Registration is currently {{ registrationOpen ? "open" : "closed" }}

When closed, new users can only be added by an administrator.

Invite User

Send an invitation link to allow someone to register, even when public registration is closed.

Pending Invitations

Email Sent Expires Actions
{{ formatUserDate(inv.created_at) }} {{ formatUserDate(inv.expires_at) }}

Users

Loading users...
No users found.
Username Email Role Joined Actions
{{ u.username }} {{ u.role }} {{ formatUserDate(u.created_at) }}
``` - [ ] **Step 3: Add Users panel CSS to SettingsView** Copy the following CSS from `UserManagementView.vue` into `SettingsView.vue`'s `