diff --git a/.gitignore b/.gitignore index b86c413..507f415 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ settings.local.json # Claude Code .claude/ +docs/superpowers/ # Environment .env @@ -32,3 +33,4 @@ docker-compose.override.yml # Misc *.log .DS_Store +.superpowers/ diff --git a/docs/superpowers/plans/2026-03-10-settings-rework.md b/docs/superpowers/plans/2026-03-10-settings-rework.md deleted file mode 100644 index 0d1f1d5..0000000 --- a/docs/superpowers/plans/2026-03-10-settings-rework.md +++ /dev/null @@ -1,1203 +0,0 @@ -# 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

- - - - - - - - - - - - - - - - - -
EmailSentExpiresActions
{{ formatUserDate(inv.created_at) }}{{ formatUserDate(inv.expires_at) }} - -
-
-
- -
-

Users

-
Loading users...
-
No users found.
- - - - - - - - - - - - - - - - - - - -
UsernameEmailRoleJoinedActions
{{ 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 `