refactor: DRY pass on frontend — shared palette, composable, and tab loader

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 22:43:59 -04:00
parent 2b2e5c666a
commit 699e525cb9
7 changed files with 95 additions and 69 deletions
+8 -9
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { ref, computed, onMounted, watch } from 'vue'
import { useBackgroundRefresh } from '@/composables/useBackgroundRefresh'
import { useChatStore } from '@/stores/chat'
import ChatMessage from '@/components/ChatMessage.vue'
import WeatherCard from '@/components/WeatherCard.vue'
@@ -190,10 +191,7 @@ function toMsg(m: BriefingMessage): Message {
}
// ─── Background refresh (no-flicker) ─────────────────────────────────────────
let _refreshTimer: ReturnType<typeof setInterval> | null = null
async function _backgroundRefreshMessages() {
if (document.hidden || chatStore.streaming || !isToday.value || !todayConvId.value) return
try {
const today = await getBriefingToday()
if (!today) return
@@ -206,14 +204,15 @@ async function _backgroundRefreshMessages() {
} catch { /* silent — don't disturb the UI on network hiccup */ }
}
useBackgroundRefresh(
_backgroundRefreshMessages,
60_000,
() => !chatStore.streaming && isToday.value && !!todayConvId.value,
)
onMounted(async () => {
await checkSetup()
if (!showWizard.value) await loadAll()
_refreshTimer = setInterval(_backgroundRefreshMessages, 60_000)
})
onUnmounted(() => {
if (_refreshTimer !== null) clearInterval(_refreshTimer)
})
</script>
+5 -19
View File
@@ -1,6 +1,8 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue";
import { apiGet, listEvents } from "@/api/client";
import { useBackgroundRefresh } from "@/composables/useBackgroundRefresh";
import { milestoneColor } from "@/utils/palette";
import type { Note } from "@/types/note";
import type { Task, TaskListResponse, TaskStatus } from "@/types/task";
import type { ToolCallRecord, Message } from "@/types/chat";
@@ -69,24 +71,8 @@ const upcomingEvents = ref<EventEntry[]>([]);
const eventSlideOverOpen = ref(false);
const editingEvent = ref<EventEntry | null>(null);
// ─── Milestone color palette ──────────────────────────────────────────────────
function milestoneColor(index: number): string {
const palette = [
"var(--color-primary)",
"var(--color-success, #22c55e)",
"#c98a00",
"var(--color-danger, #e74c3c)",
"#8b5cf6",
];
return palette[index % palette.length];
}
// ─── Background refresh (no-flicker) ─────────────────────────────────────────
// Runs on a timer while the page is visible. Never touches `loading` so
// existing content stays on screen while the fetch is in flight.
let _refreshTimer: ReturnType<typeof setInterval> | null = null
// Never touches `loading` so existing content stays on screen while fetching.
function _dateRange() {
const today = new Date()
@@ -173,9 +159,10 @@ onMounted(async () => {
chatInputRef.value?.focus();
loadProjects();
_refreshTimer = setInterval(_backgroundRefresh, 90_000)
});
useBackgroundRefresh(_backgroundRefresh, 90_000, () => !loading.value);
async function loadProjects() {
if (!heroProject.value) return;
const hid = heroProject.value.id;
@@ -239,7 +226,6 @@ onMounted(() => {
});
onUnmounted(() => {
document.removeEventListener("shortcut:focus-chat", onFocusChatShortcut);
if (_refreshTimer !== null) clearInterval(_refreshTimer)
});
const chatStore = useChatStore();
+1 -11
View File
@@ -3,6 +3,7 @@ import { ref, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import { apiGet, apiPost } from "@/api/client";
import { useToastStore } from "@/stores/toast";
import { milestoneColor } from "@/utils/palette";
interface MilestoneSummary {
id: number;
@@ -73,17 +74,6 @@ async function loadProjects() {
}
}
function milestoneColor(index: number): string {
const palette = [
"var(--color-primary)",
"var(--color-success)",
"#c98a00",
"#8b5cf6",
"#ef4444",
"#06b6d4",
];
return palette[index % palette.length];
}
onMounted(loadProjects);
+21 -30
View File
@@ -3,7 +3,7 @@ import { ref, watch, onMounted } from "vue";
import { useSettingsStore } from "@/stores/settings";
import { useAuthStore } from "@/stores/auth";
import { useToastStore } from "@/stores/toast";
import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, getBriefingConfig, saveBriefingConfig, getBriefingFeeds, createBriefingFeed, deleteBriefingFeed, refreshBriefingFeeds, geocodeAddress, getFableMcpInfo, type GroupEntry, type GroupMember, type UserSearchResult, type BriefingConfig, type BriefingFeed } from "@/api/client";
import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, getBriefingConfig, saveBriefingConfig, getBriefingFeeds, createBriefingFeed, deleteBriefingFeed, refreshBriefingFeeds, geocodeAddress, getFableMcpInfo, listApiKeys, createApiKey as apiCreateApiKey, revokeApiKey as apiRevokeApiKey, type ApiKeyEntry, type GroupEntry, type GroupMember, type UserSearchResult, type BriefingConfig, type BriefingFeed } from "@/api/client";
import { usePushStore } from "@/stores/push";
import type { User } from "@/types/auth";
import PaginationBar from "@/components/PaginationBar.vue";
@@ -43,17 +43,24 @@ const restoreFileInput = ref<HTMLInputElement | null>(null);
const VALID_TABS = new Set(["general", "account", "notifications", "integrations", "data", "briefing", "apikeys", "config", "users", "logs", "groups"]);
const _stored = localStorage.getItem("settings_tab") ?? "general";
const activeTab = ref(VALID_TABS.has(_stored) ? (_stored === "admin" ? "config" : _stored) : "general");
function _loadTabContent(tab: string) {
if (authStore.isAdmin) {
if (tab === "users") loadUsersPanel();
else if (tab === "logs") loadLogsPanel();
else if (tab === "groups") loadGroupsPanel();
}
if (tab === "briefing") loadBriefingTab();
if (tab === "apikeys") { fetchApiKeys(); loadMcpInfo(); }
}
watch(activeTab, (v) => {
localStorage.setItem("settings_tab", v === "admin" ? "config" : v);
if (v === "users" && authStore.isAdmin) loadUsersPanel();
if (v === "logs" && authStore.isAdmin) loadLogsPanel();
if (v === "groups" && authStore.isAdmin) loadGroupsPanel();
if (v === "briefing") loadBriefingTab();
if (v === "apikeys") { fetchApiKeys(); loadMcpInfo(); }
_loadTabContent(v);
});
// API Keys
const apiKeys = ref<Array<{id: number, name: string, scope: string, key_prefix: string, last_used_at: string | null}>>([]);
const apiKeys = ref<ApiKeyEntry[]>([]);
const newKeyName = ref('');
const newKeyScope = ref<'read' | 'write'>('write');
const newKeyValue = ref('');
@@ -89,36 +96,24 @@ async function loadMcpInfo() {
}
async function fetchApiKeys() {
const res = await fetch('/api/api-keys', { credentials: 'include' });
if (res.ok) {
const data = await res.json();
apiKeys.value = data.api_keys;
}
apiKeys.value = await listApiKeys();
}
async function createApiKey() {
if (!newKeyName.value) return;
creatingApiKey.value = true;
try {
const res = await fetch('/api/api-keys', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newKeyName.value, scope: newKeyScope.value }),
});
if (res.ok) {
const data = await res.json();
newKeyValue.value = data.key;
newKeyName.value = '';
await fetchApiKeys();
}
const data = await apiCreateApiKey(newKeyName.value, newKeyScope.value);
newKeyValue.value = data.key;
newKeyName.value = '';
await fetchApiKeys();
} finally {
creatingApiKey.value = false;
}
}
async function revokeApiKey(id: number) {
await fetch(`/api/api-keys/${id}`, { method: 'DELETE', credentials: 'include' });
await apiRevokeApiKey(id);
revokeConfirmId.value = null;
await fetchApiKeys();
}
@@ -521,12 +516,8 @@ onMounted(async () => {
} catch {
// base URL not configured yet
}
if (activeTab.value === "groups") loadGroupsPanel();
if (activeTab.value === "users") loadUsersPanel();
if (activeTab.value === "logs") loadLogsPanel();
}
if (activeTab.value === "briefing") loadBriefingTab();
if (activeTab.value === "apikeys") { fetchApiKeys(); loadMcpInfo(); }
_loadTabContent(activeTab.value);
});
async function changeEmail() {