lvwerra HF Staff commited on
Commit
77ea377
·
verified ·
1 Parent(s): 7f71894

Sync: usage refresh + snapshot age

Browse files
__pycache__/setup.cpython-313.pyc ADDED
Binary file (1.99 kB). View file
 
web/src/api.ts CHANGED
@@ -38,7 +38,7 @@ export const getInfo = () => fetch('/api/info').then(json);
38
  export interface QuotaWindow { usedPercent?: number; resetsAt?: number; windowMinutes?: number; }
39
  export interface ProviderUsage {
40
  tokensToday?: number; costToday?: number; tokensWeek?: number; costWeek?: number; totalCost?: number;
41
- quota?: { fiveHour?: QuotaWindow; weekly?: QuotaWindow; opus?: QuotaWindow } | null;
42
  }
43
  export interface Usage { providers: Record<string, ProviderUsage>; generatedAt: string; }
44
  export const getUsage = (): Promise<Usage> => fetch('/api/usage').then(json);
 
38
  export interface QuotaWindow { usedPercent?: number; resetsAt?: number; windowMinutes?: number; }
39
  export interface ProviderUsage {
40
  tokensToday?: number; costToday?: number; tokensWeek?: number; costWeek?: number; totalCost?: number;
41
+ quota?: { fiveHour?: QuotaWindow; weekly?: QuotaWindow; opus?: QuotaWindow; updatedAt?: number } | null;
42
  }
43
  export interface Usage { providers: Record<string, ProviderUsage>; generatedAt: string; }
44
  export const getUsage = (): Promise<Usage> => fetch('/api/usage').then(json);
web/src/components/UsagePanel.tsx CHANGED
@@ -18,6 +18,14 @@ const resetStr = (s?: number) => {
18
  if (mins < 60) return `resets in ${mins}m`;
19
  return `resets in ${Math.floor(mins / 60)}h ${mins % 60}m`;
20
  };
 
 
 
 
 
 
 
 
21
 
22
  function Bar({ label, q }: { label: string; q?: QuotaWindow }) {
23
  if (!q || q.usedPercent == null) return null;
@@ -34,13 +42,32 @@ function Bar({ label, q }: { label: string; q?: QuotaWindow }) {
34
  export default function UsagePanel() {
35
  const [u, setU] = useState<Usage | null>(null);
36
  const [err, setErr] = useState(false);
37
- useEffect(() => { api.getUsage().then(setU).catch(() => setErr(true)); }, []);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
  if (err) return <div className="placeholder"><p>Usage data unavailable (is <span className="mono">ccusage</span> installed?).</p></div>;
40
  if (!u) return <div className="placeholder"><p>Loading…</p></div>;
41
 
42
  return (
43
  <div className="usage">
 
 
 
 
 
44
  {PROVS.map((p) => {
45
  const d = u.providers[p.id] || {};
46
  const q = d.quota;
@@ -59,6 +86,7 @@ export default function UsagePanel() {
59
  <Bar label="5-hour" q={q.fiveHour} />
60
  <Bar label="Weekly" q={q.weekly} />
61
  {!q.fiveHour && !q.weekly && <div className="s-help">No quota snapshot yet — run a session to populate.</div>}
 
62
  </div>
63
  ) : p.id === 'gemini' ? (
64
  <div className="s-help">No quota (consumer tier deprecated — uses an API key).</div>
@@ -69,7 +97,7 @@ export default function UsagePanel() {
69
  );
70
  })}
71
  <div className="s-help">
72
- Tokens/cost are read from local logs; cost is an <em>estimated API-equivalent</em> (subscriptions are flat-fee). Quota % is the live 5-hour / weekly window where the agent exposes it.
73
  </div>
74
  </div>
75
  );
 
18
  if (mins < 60) return `resets in ${mins}m`;
19
  return `resets in ${Math.floor(mins / 60)}h ${mins % 60}m`;
20
  };
21
+ const agoStr = (ms?: number) => {
22
+ if (!ms) return '';
23
+ const m = Math.round((Date.now() - ms) / 60000);
24
+ if (m < 1) return 'just now';
25
+ if (m < 60) return `${m}m ago`;
26
+ const h = Math.floor(m / 60);
27
+ return h < 24 ? `${h}h ago` : `${Math.floor(h / 24)}d ago`;
28
+ };
29
 
30
  function Bar({ label, q }: { label: string; q?: QuotaWindow }) {
31
  if (!q || q.usedPercent == null) return null;
 
42
  export default function UsagePanel() {
43
  const [u, setU] = useState<Usage | null>(null);
44
  const [err, setErr] = useState(false);
45
+ const [busy, setBusy] = useState(false);
46
+
47
+ const load = () => {
48
+ setBusy(true);
49
+ return api.getUsage().then((d) => { setU(d); setErr(false); }).catch(() => setErr(true)).finally(() => setBusy(false));
50
+ };
51
+ // Refresh on open, on a timer, and when the tab regains focus — the underlying
52
+ // numbers only change when an agent runs, so this just keeps the view current.
53
+ useEffect(() => {
54
+ load();
55
+ const t = setInterval(load, 30_000);
56
+ const onFocus = () => load();
57
+ window.addEventListener('focus', onFocus);
58
+ return () => { clearInterval(t); window.removeEventListener('focus', onFocus); };
59
+ }, []);
60
 
61
  if (err) return <div className="placeholder"><p>Usage data unavailable (is <span className="mono">ccusage</span> installed?).</p></div>;
62
  if (!u) return <div className="placeholder"><p>Loading…</p></div>;
63
 
64
  return (
65
  <div className="usage">
66
+ <div className="usage-top">
67
+ <span className="s-muted">Auto-refreshes every 30s</span>
68
+ <span className="spacer" />
69
+ <button className="btn-ghost" onClick={load} disabled={busy}>{busy ? '…' : '↻ Refresh'}</button>
70
+ </div>
71
  {PROVS.map((p) => {
72
  const d = u.providers[p.id] || {};
73
  const q = d.quota;
 
86
  <Bar label="5-hour" q={q.fiveHour} />
87
  <Bar label="Weekly" q={q.weekly} />
88
  {!q.fiveHour && !q.weekly && <div className="s-help">No quota snapshot yet — run a session to populate.</div>}
89
+ {q.updatedAt && <div className="s-help">Snapshot {agoStr(q.updatedAt)} — refreshes when {p.label} runs.</div>}
90
  </div>
91
  ) : p.id === 'gemini' ? (
92
  <div className="s-help">No quota (consumer tier deprecated — uses an API key).</div>
 
97
  );
98
  })}
99
  <div className="s-help">
100
+ Tokens/cost are read from local logs; cost is an <em>estimated API-equivalent</em> (subscriptions are flat-fee). Quota % is a <em>snapshot</em> captured the last time each agent ran (Claude via its status line, Codex from its logs) — it won't reflect an external reset until you run that agent again.
101
  </div>
102
  </div>
103
  );
web/src/styles.css CHANGED
@@ -278,6 +278,10 @@ body {
278
 
279
  /* usage page */
280
  .usage { display: flex; flex-direction: column; gap: 12px; margin-top: 12px; }
 
 
 
 
281
  .usage-card { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; }
282
  .usage-head { display: flex; align-items: center; gap: 9px; font-size: 14px; }
283
  .usage-head .spacer { flex: 1; }
 
278
 
279
  /* usage page */
280
  .usage { display: flex; flex-direction: column; gap: 12px; margin-top: 12px; }
281
+ .usage-top { display: flex; align-items: center; gap: 8px; }
282
+ .usage-top .spacer { flex: 1; }
283
+ .usage-top .s-muted { font-size: 12px; }
284
+ .usage-top .btn-ghost { padding: 5px 10px; font-size: 12px; }
285
  .usage-card { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; }
286
  .usage-head { display: flex; align-items: center; gap: 9px; font-size: 14px; }
287
  .usage-head .spacer { flex: 1; }