From 26a8fb5c5158c54e6a1320d5bad5e8d9cb484a79 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 26 Mar 2026 22:24:37 -0400 Subject: [PATCH 1/6] fix: load API keys and MCP info on mount when tab is pre-selected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetchApiKeys() and loadMcpInfo() were only wired to the activeTab watcher, which fires on changes but not on initial mount. If localStorage had 'apikeys' as the last tab, both calls were skipped entirely — causing an empty key list and no whl download button. --- frontend/src/views/SettingsView.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 0b011e3..1245690 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -524,6 +524,7 @@ onMounted(async () => { if (activeTab.value === "groups") loadGroupsPanel(); if (activeTab.value === "briefing") loadBriefingTab(); } + if (activeTab.value === "apikeys") { fetchApiKeys(); loadMcpInfo(); } }); async function changeEmail() { From 2b2e5c666a29545782fdb1fbaec2d34e4c2adf57 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 26 Mar 2026 22:32:28 -0400 Subject: [PATCH 2/6] fix: load all pre-selected settings tabs correctly on mount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The watch(activeTab) handler loads tab data on navigation, but not on initial mount when localStorage restores a tab. Three more gaps: - briefing: was inside the isAdmin guard — non-admin users who last visited the Briefing tab would see an empty form - users: no onMounted equivalent — admin user list never loaded - logs: no onMounted equivalent — admin log viewer never loaded Moves briefing outside the admin guard and adds users/logs inside it. --- frontend/src/views/SettingsView.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 1245690..878ae8e 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -522,8 +522,10 @@ onMounted(async () => { // base URL not configured yet } if (activeTab.value === "groups") loadGroupsPanel(); - if (activeTab.value === "briefing") loadBriefingTab(); + if (activeTab.value === "users") loadUsersPanel(); + if (activeTab.value === "logs") loadLogsPanel(); } + if (activeTab.value === "briefing") loadBriefingTab(); if (activeTab.value === "apikeys") { fetchApiKeys(); loadMcpInfo(); } }); From 699e525cb9e3cde2c1fa29560227bff5816e3fbc Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 26 Mar 2026 22:43:59 -0400 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20DRY=20pass=20on=20frontend=20?= =?UTF-8?q?=E2=80=94=20shared=20palette,=20composable,=20and=20tab=20loade?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract milestoneColor to utils/palette.ts; remove duplicate in HomeView + ProjectListView - Create useBackgroundRefresh composable; wire into HomeView + BriefingView (removes manual setInterval/clearInterval boilerplate) - Extract _loadTabContent() in SettingsView so watch and onMounted share one tab→loader mapping - Move raw fetch() api-key calls to typed helpers in api/client.ts (listApiKeys, createApiKey, revokeApiKey) - Drop unused onUnmounted import from BriefingView Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/api/client.ts | 18 +++++++ .../src/composables/useBackgroundRefresh.ts | 29 +++++++++++ frontend/src/utils/palette.ts | 13 +++++ frontend/src/views/BriefingView.vue | 17 +++---- frontend/src/views/HomeView.vue | 24 ++------- frontend/src/views/ProjectListView.vue | 12 +---- frontend/src/views/SettingsView.vue | 51 ++++++++----------- 7 files changed, 95 insertions(+), 69 deletions(-) create mode 100644 frontend/src/composables/useBackgroundRefresh.ts create mode 100644 frontend/src/utils/palette.ts diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 5ae97d0..78e565a 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -580,3 +580,21 @@ export async function updateEvent(id: number, payload: EventUpdatePayload): Prom export async function deleteEvent(id: number): Promise { return apiDelete(`/api/events/${id}`); } + +// ─── API Keys ───────────────────────────────────────────────────────────────── + +export interface ApiKeyEntry { + id: number + name: string + scope: string + key_prefix: string + last_used_at: string | null +} + +export const listApiKeys = () => + apiGet<{ api_keys: ApiKeyEntry[] }>('/api/api-keys').then(r => r.api_keys) + +export const createApiKey = (name: string, scope: 'read' | 'write') => + apiPost<{ key: string; api_key: ApiKeyEntry }>('/api/api-keys', { name, scope }) + +export const revokeApiKey = (id: number) => apiDelete(`/api/api-keys/${id}`) diff --git a/frontend/src/composables/useBackgroundRefresh.ts b/frontend/src/composables/useBackgroundRefresh.ts new file mode 100644 index 0000000..9aa2912 --- /dev/null +++ b/frontend/src/composables/useBackgroundRefresh.ts @@ -0,0 +1,29 @@ +import { onMounted, onUnmounted } from 'vue' + +/** + * Runs `refreshFn` on a recurring interval while the page is visible. + * Safe to use in any component — timer is cleared on unmount. + * + * @param refreshFn Called each tick. Should be silent (no loading state changes). + * @param intervalMs Polling interval in milliseconds. + * @param canRun Optional guard — refresh is skipped when this returns false. + */ +export function useBackgroundRefresh( + refreshFn: () => void, + intervalMs: number, + canRun?: () => boolean, +): void { + let timer: ReturnType | null = null + + onMounted(() => { + timer = setInterval(() => { + if (document.hidden) return + if (canRun && !canRun()) return + refreshFn() + }, intervalMs) + }) + + onUnmounted(() => { + if (timer !== null) clearInterval(timer) + }) +} diff --git a/frontend/src/utils/palette.ts b/frontend/src/utils/palette.ts new file mode 100644 index 0000000..e04101d --- /dev/null +++ b/frontend/src/utils/palette.ts @@ -0,0 +1,13 @@ +/** Cyclic color palette for milestone progress bars. */ +export const MILESTONE_PALETTE = [ + 'var(--color-primary)', + 'var(--color-success, #22c55e)', + '#c98a00', + '#8b5cf6', + 'var(--color-danger, #ef4444)', + '#06b6d4', +] + +export function milestoneColor(index: number): string { + return MILESTONE_PALETTE[index % MILESTONE_PALETTE.length] +} diff --git a/frontend/src/views/BriefingView.vue b/frontend/src/views/BriefingView.vue index 63c236d..d3c0fce 100644 --- a/frontend/src/views/BriefingView.vue +++ b/frontend/src/views/BriefingView.vue @@ -1,5 +1,6 @@ diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 40664c3..ca79eda 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -1,6 +1,8 @@