From 1f309ccca20aabbd0ee3fb8fbb3c8192124edd95 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Thu, 21 May 2026 15:44:00 +0900 Subject: fix: wake scheduler persistence/retry, credential filtering, usage cache and display names - Wake scheduler: fix Bun timer leak, make recurring daily, persist to disk, retry failed wakes every 5min for 30min, start at boot - Key usage: localStorage cache survives page refresh, spinner during all refreshes, show cached data immediately - Credential filtering: key-usage and wake only use configured credentials_file, exclude unconfigured accounts - Display: remove counter suffix from Claude labels, format opencode/copilot key names --- .../frontend/src/lib/components/KeyUsage.svelte | 116 +++++++++++++++------ .../src/lib/components/SidebarPanel.svelte | 15 ++- 2 files changed, 97 insertions(+), 34 deletions(-) (limited to 'packages/frontend/src/lib/components') diff --git a/packages/frontend/src/lib/components/KeyUsage.svelte b/packages/frontend/src/lib/components/KeyUsage.svelte index 5e86f36..a2b735e 100644 --- a/packages/frontend/src/lib/components/KeyUsage.svelte +++ b/packages/frontend/src/lib/components/KeyUsage.svelte @@ -17,8 +17,45 @@ loading: boolean; } - // Module-level cache survives remounts during the same page session - const usageCache = new Map(); + // localStorage-backed cache: survives page refreshes + const CACHE_STORAGE_KEY = "dispatch-key-usage-cache"; + + function loadPersistedCache(): Map { + try { + const raw = localStorage.getItem(CACHE_STORAGE_KEY); + if (raw) { + const parsed = JSON.parse(raw) as Record; + return new Map(Object.entries(parsed)); + } + } catch { + // Ignore parse errors + } + return new Map(); + } + + function persistCache(cache: Map): void { + try { + const obj = Object.fromEntries(cache.entries()); + localStorage.setItem(CACHE_STORAGE_KEY, JSON.stringify(obj)); + } catch { + // Ignore storage errors (e.g. quota exceeded) + } + } + + const usageCache = loadPersistedCache(); + + function buildEntries(keyList: KeyInfo[]): KeyUsageEntry[] { + return keyList.map((k) => { + const cached = usageCache.get(k.id); + return { + keyId: k.id, + provider: k.provider, + data: cached ?? null, + error: null, + loading: true, // always show spinner during refresh + }; + }); + } let entries = $state([]); @@ -36,6 +73,7 @@ } else { const fresh = await res.json() as KeyUsageData; usageCache.set(key.id, fresh); + persistCache(usageCache); updateEntry(key.id, { data: fresh, error: null, @@ -59,26 +97,23 @@ ); } + // Sync entries with keys reactively — runs before DOM update so + // cached data renders on first paint without a flash of empty state. + $effect.pre(() => { + entries = buildEntries(keys); + }); + + // Fetch data and set up 90s auto-refresh $effect(() => { - // Show cached data immediately if available, then refresh in background - entries = keys.map((k) => { - const cached = usageCache.get(k.id); - return { - keyId: k.id, - provider: k.provider, - data: cached ?? null, - error: null, - loading: true, - }; - }); + const currentKeys = keys; // Fire all fetches in parallel - for (const key of keys) { + for (const key of currentKeys) { fetchOne(key); } // Refresh every 90s const interval = setInterval(() => { - for (const key of keys) { + for (const key of currentKeys) { updateEntry(key.id, { loading: true }); fetchOne(key); } @@ -115,6 +150,9 @@ const claudeLoading = $derived( entries.some((e) => e.provider === "anthropic" && e.loading), ); + const claudeError = $derived( + entries.find((e) => e.provider === "anthropic" && e.error)?.error ?? null, + ); const nonClaudeEntries = $derived( entries.filter((e) => e.provider !== "anthropic"), @@ -160,6 +198,14 @@ }); } + function formatKeyId(id: string): string { + if (/^github-copilot/i.test(id)) return "Copilot"; + return id.split("-").map((part) => { + if (part.toLowerCase() === "opencode") return "OpenCode"; + return part.charAt(0).toUpperCase() + part.slice(1); + }).join(" "); + } + function hasBucketData(bucket: UsageBucket | undefined): boolean { return bucket !== undefined && bucket.utilization !== undefined; } @@ -171,7 +217,7 @@ {:else}
- {#if claudeAccounts.length > 0 || claudeLoading} + {#if claudeAccounts.length > 0 || claudeLoading || claudeError}
Claude @@ -181,12 +227,17 @@ {/if}
- {#if claudeAccounts.length === 0 && claudeLoading} -
- Loading... -
- {:else} - {#each claudeAccounts as acct, idx (acct.source)} + {#if claudeAccounts.length === 0 && claudeLoading} +
+ Loading... +
+ {:else if claudeAccounts.length === 0 && claudeError} + + {:else} + {#if claudeError} + + {/if} + {#each claudeAccounts as acct, idx (acct.source)} {#if idx > 0}
{/if} @@ -240,26 +291,26 @@ {#each nonClaudeEntries as entry (entry.keyId)}
- {entry.keyId} + {formatKeyId(entry.keyId)} {entry.provider} {#if entry.loading} {/if}
- {#if entry.loading} + {#if entry.loading && !entry.data}
Loading...
+ {:else} + {#if entry.error} + + {/if} + {#if !entry.data} +

No data.

- {:else if entry.error} - - - {:else if !entry.data} -

No data.

- - {:else if entry.data.provider === "opencode-go"} + {:else if entry.data.provider === "opencode-go"} {#if entry.data.unavailable}

Check console for usage.

{#if entry.data.consoleUrl} @@ -335,7 +386,8 @@ Resets: {formatDate(entry.data.resetAt)} {/if}
- {/if} + {/if} + {/if}
{/each}
diff --git a/packages/frontend/src/lib/components/SidebarPanel.svelte b/packages/frontend/src/lib/components/SidebarPanel.svelte index aebc74a..6edbdbf 100644 --- a/packages/frontend/src/lib/components/SidebarPanel.svelte +++ b/packages/frontend/src/lib/components/SidebarPanel.svelte @@ -50,11 +50,22 @@ function addPanel() { panels = [...panels, { id: nextId++, selected: "Select a view" }]; } + + function panelClass(selected: string): string { + const base = "bg-base-200 rounded-lg p-3 flex flex-col min-h-0"; + const fill = selected === "Key Usage" || selected === "Claude Reset" || selected === "Tasks"; + return fill ? base + " flex-1" : base; + } + + function contentClass(selected: string): string { + const fill = selected === "Key Usage" || selected === "Claude Reset" || selected === "Tasks"; + return fill ? "mt-2 flex-1 min-h-0" : "mt-2"; + }
{#each panels as panel, idx (panel.id)} -
+