summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/components/KeyUsage.svelte
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-20 23:37:01 +0900
committerAdam Malczewski <[email protected]>2026-05-20 23:37:01 +0900
commit1f4776e6891348d2dbdcbbf704c0a5901b008ecf (patch)
treed406c2ba412b5393b16a3f9253acea34807d0dda /packages/frontend/src/lib/components/KeyUsage.svelte
parent29e4c2db4555bb764f458971f0c591c092e30ed6 (diff)
downloaddispatch-1f4776e6891348d2dbdcbbf704c0a5901b008ecf.tar.gz
dispatch-1f4776e6891348d2dbdcbbf704c0a5901b008ecf.zip
feat: Claude Reset scheduler + fix key config and Claude grouping
- Added claude-pro key pointing to default credentials, claude-max pointing to .credentials-2.json (docker path /root/.claude/) - POST /models/wake sends 'hi' to haiku for all Claude accounts - ClaudeReset.svelte: 2 AM rows + 2 PM rows of 6 hour blocks each (12-hour American format). Click blocks to schedule wake at :15 - Key Usage now groups all Claude accounts under one 'Claude' card instead of duplicating under claude-pro and claude-max cards
Diffstat (limited to 'packages/frontend/src/lib/components/KeyUsage.svelte')
-rw-r--r--packages/frontend/src/lib/components/KeyUsage.svelte157
1 files changed, 102 insertions, 55 deletions
diff --git a/packages/frontend/src/lib/components/KeyUsage.svelte b/packages/frontend/src/lib/components/KeyUsage.svelte
index 3587ff8..51e08c2 100644
--- a/packages/frontend/src/lib/components/KeyUsage.svelte
+++ b/packages/frontend/src/lib/components/KeyUsage.svelte
@@ -71,6 +71,39 @@
}
});
+ // Merge duplicate Claude entries — all anthropic keys return the same
+ // set of accounts, so collect and deduplicate under one "Claude" card.
+ const claudeAccounts = $derived.by(() => {
+ const seen = new Set<string>();
+ const accounts: Array<{
+ label: string;
+ source: string;
+ subscriptionType?: string;
+ fiveHour?: UsageBucket;
+ sevenDay?: UsageBucket;
+ error?: string;
+ }> = [];
+ const claudeEntries = entries.filter((e) => e.provider === "anthropic");
+ for (const e of claudeEntries) {
+ if (!e.data || e.data.provider !== "anthropic" || !e.data.accounts) continue;
+ for (const acct of e.data.accounts) {
+ if (!seen.has(acct.source)) {
+ seen.add(acct.source);
+ accounts.push(acct);
+ }
+ }
+ }
+ return accounts;
+ });
+
+ const claudeLoading = $derived(
+ entries.some((e) => e.provider === "anthropic" && e.loading),
+ );
+
+ const nonClaudeEntries = $derived(
+ entries.filter((e) => e.provider !== "anthropic"),
+ );
+
function progressClass(utilization: number): string {
if (utilization > 0.8) return "progress-error";
if (utilization >= 0.5) return "progress-warning";
@@ -91,7 +124,75 @@
<p class="text-xs text-base-content/50">No keys available.</p>
{:else}
<div class="flex flex-col gap-3 max-h-96 overflow-y-auto">
- {#each entries as entry (entry.keyId)}
+ <!-- Claude (all accounts merged under one card) -->
+ {#if claudeLoading}
+ <div class="bg-base-200 rounded-lg p-2">
+ <div class="flex items-center gap-1.5 mb-1.5">
+ <span class="text-xs font-semibold">Claude</span>
+ <span class="badge badge-xs badge-ghost">anthropic</span>
+ </div>
+ <div class="flex items-center gap-1.5 py-1">
+ <span class="loading loading-spinner loading-xs"></span>
+ <span class="text-xs text-base-content/50">Loading...</span>
+ </div>
+ </div>
+ {:else if claudeAccounts.length > 0}
+ <div class="bg-base-200 rounded-lg p-2">
+ <div class="flex items-center gap-1.5 mb-1.5">
+ <span class="text-xs font-semibold">Claude</span>
+ <span class="badge badge-xs badge-ghost">anthropic</span>
+ </div>
+ {#each claudeAccounts as acct, idx (acct.source)}
+ {#if idx > 0}
+ <div class="border-t border-base-300 my-1.5"></div>
+ {/if}
+ <div class="flex flex-col gap-1 pl-1">
+ <div class="flex items-center gap-1">
+ <span class="text-xs font-medium">{acct.label}</span>
+ {#if acct.subscriptionType}
+ <span class="badge badge-xs">{acct.subscriptionType}</span>
+ {/if}
+ </div>
+ {#if acct.error}
+ <p class="text-xs text-error/70">{acct.error}</p>
+ {/if}
+ {#if hasBucketData(acct.fiveHour)}
+ {@const b = acct.fiveHour!}
+ {@const u = b.utilization ?? 0}
+ {@const p = Math.round(u * 100)}
+ <div class="flex flex-col gap-0.5">
+ <div class="flex items-center justify-between">
+ <span class="text-xs text-base-content/50">5-Hour</span>
+ <span class="text-xs font-mono">{p}%</span>
+ </div>
+ <progress class="progress w-full h-2 {progressClass(u)}" value={p} max="100"></progress>
+ {#if b.resetsAt}
+ <span class="text-xs text-base-content/40">Resets: {formatDate(b.resetsAt)}</span>
+ {/if}
+ </div>
+ {/if}
+ {#if hasBucketData(acct.sevenDay)}
+ {@const b = acct.sevenDay!}
+ {@const u = b.utilization ?? 0}
+ {@const p = Math.round(u * 100)}
+ <div class="flex flex-col gap-0.5">
+ <div class="flex items-center justify-between">
+ <span class="text-xs text-base-content/50">Weekly</span>
+ <span class="text-xs font-mono">{p}%</span>
+ </div>
+ <progress class="progress w-full h-2 {progressClass(u)}" value={p} max="100"></progress>
+ {#if b.resetsAt}
+ <span class="text-xs text-base-content/40">Resets: {formatDate(b.resetsAt)}</span>
+ {/if}
+ </div>
+ {/if}
+ </div>
+ {/each}
+ </div>
+ {/if}
+
+ <!-- Non-Claude keys -->
+ {#each nonClaudeEntries as entry (entry.keyId)}
<div class="bg-base-200 rounded-lg p-2">
<div class="flex items-center gap-1.5 mb-1.5">
<span class="text-xs font-semibold">{entry.keyId}</span>
@@ -110,60 +211,6 @@
{:else if !entry.data}
<p class="text-xs text-base-content/50">No data.</p>
- {:else if entry.data.provider === "anthropic"}
- <!-- Render each Claude account -->
- {#if entry.data.accounts}
- {#each entry.data.accounts as acct, idx (acct.source)}
- {#if idx > 0}
- <div class="border-t border-base-300 my-1.5"></div>
- {/if}
- <div class="flex flex-col gap-1 pl-1">
- <div class="flex items-center gap-1">
- <span class="text-xs font-medium">{acct.label}</span>
- {#if acct.subscriptionType}
- <span class="badge badge-xs">{acct.subscriptionType}</span>
- {/if}
- </div>
-
- {#if acct.error}
- <p class="text-xs text-error/70">{acct.error}</p>
- {/if}
-
- {#if hasBucketData(acct.fiveHour)}
- {@const b = acct.fiveHour!}
- {@const u = b.utilization ?? 0}
- {@const p = Math.round(u * 100)}
- <div class="flex flex-col gap-0.5">
- <div class="flex items-center justify-between">
- <span class="text-xs text-base-content/50">5-Hour</span>
- <span class="text-xs font-mono">{p}%</span>
- </div>
- <progress class="progress w-full h-2 {progressClass(u)}" value={p} max="100"></progress>
- {#if b.resetsAt}
- <span class="text-xs text-base-content/40">Resets: {formatDate(b.resetsAt)}</span>
- {/if}
- </div>
- {/if}
-
- {#if hasBucketData(acct.sevenDay)}
- {@const b = acct.sevenDay!}
- {@const u = b.utilization ?? 0}
- {@const p = Math.round(u * 100)}
- <div class="flex flex-col gap-0.5">
- <div class="flex items-center justify-between">
- <span class="text-xs text-base-content/50">Weekly</span>
- <span class="text-xs font-mono">{p}%</span>
- </div>
- <progress class="progress w-full h-2 {progressClass(u)}" value={p} max="100"></progress>
- {#if b.resetsAt}
- <span class="text-xs text-base-content/40">Resets: {formatDate(b.resetsAt)}</span>
- {/if}
- </div>
- {/if}
- </div>
- {/each}
- {/if}
-
{:else if entry.data.provider === "opencode-go"}
{#if entry.data.unavailable}
<p class="text-xs text-base-content/70">Check console for usage.</p>