Spaces:
Running
Running
Sync: usage refresh + snapshot age
Browse files- __pycache__/setup.cpython-313.pyc +0 -0
- web/src/api.ts +1 -1
- web/src/components/UsagePanel.tsx +30 -2
- web/src/styles.css +4 -0
__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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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; }
|