claude-usage-dashboard 1.3.11 → 1.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/cli.cjs +8 -7
- package/package.json +1 -1
- package/public/js/app.js +43 -12
- package/public/js/charts/quota-gauge.js +11 -2
- package/server/aggregator.js +7 -3
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ A self-hosted dashboard that visualizes your [Claude Code](https://claude.ai/cod
|
|
|
10
10
|
|
|
11
11
|
- **Token tracking** — Total tokens with breakdown by input, output, cache read, and cache write
|
|
12
12
|
- **Cost estimation** — API cost equivalent at standard pricing
|
|
13
|
-
- **Subscription quota** — Real-time utilization gauges (5-hour, 7-day, per-model) pulled from the Anthropic API with auto-detection of your plan tier. Includes projected API cost at 100% quota utilization (weekly and monthly)
|
|
13
|
+
- **Subscription quota** — Real-time utilization gauges (5-hour, 7-day, per-model) pulled from the Anthropic API with auto-detection of your plan tier. Includes projected API cost at 100% quota utilization (weekly and monthly), calculated using the exact quota window derived from the 7-day reset time. Shows the quota window date range for transparency. Date picker defaults to the current quota period on page load
|
|
14
14
|
- **Token consumption trend** — Stacked bar chart with hourly, daily, weekly, or monthly granularity. Toggle between tokens and dollar view. Includes period summary with avg/min/max stats, active hours heatmap, and smart date range limits per granularity
|
|
15
15
|
- **Model distribution** — Donut chart showing usage across Claude models
|
|
16
16
|
- **Cache efficiency** — Visual breakdown of cache read, cache creation, and uncached requests
|
package/bin/cli.cjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
const { join } = require('path');
|
|
4
|
-
const {
|
|
5
|
-
|
|
6
|
-
const serverPath = join(__dirname, '..', 'server', 'index.js');
|
|
7
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const { join } = require('path');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
const serverPath = join(__dirname, '..', 'server', 'index.js');
|
|
7
|
+
const result = spawnSync(process.execPath, [serverPath], { stdio: 'inherit' });
|
|
8
|
+
process.exit(result.status || 0);
|
package/package.json
CHANGED
package/public/js/app.js
CHANGED
|
@@ -47,18 +47,40 @@ function getTimezoneAbbr() {
|
|
|
47
47
|
return tz ? tz.value : '';
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Derive the 7-day quota window from resets_at, truncated to the hour
|
|
51
|
+
function getQuotaWindow(sevenDay) {
|
|
52
|
+
if (!sevenDay?.resets_at) return null;
|
|
53
|
+
const resetsAt = new Date(sevenDay.resets_at);
|
|
54
|
+
resetsAt.setMinutes(0, 0, 0);
|
|
55
|
+
const windowStart = new Date(resetsAt);
|
|
56
|
+
windowStart.setDate(windowStart.getDate() - 7);
|
|
57
|
+
return { from: windowStart, to: resetsAt };
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
async function loadQuota() {
|
|
51
61
|
try {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
const data = await fetchQuota();
|
|
63
|
+
|
|
64
|
+
// Use the actual quota window (resets_at - 7 days → resets_at)
|
|
65
|
+
let cost7dValue = 0;
|
|
66
|
+
let quotaWindowFrom = null;
|
|
67
|
+
let quotaWindowTo = null;
|
|
68
|
+
const sevenDay = data.seven_day;
|
|
69
|
+
const window = getQuotaWindow(sevenDay);
|
|
70
|
+
if (window && sevenDay.utilization > 0) {
|
|
71
|
+
quotaWindowFrom = window.from;
|
|
72
|
+
quotaWindowTo = window.to;
|
|
73
|
+
const cost7d = await fetchCost({
|
|
74
|
+
from: window.from.toISOString(),
|
|
75
|
+
to: window.to.toISOString(),
|
|
76
|
+
plan: state.plan.plan,
|
|
77
|
+
});
|
|
78
|
+
cost7dValue = cost7d.api_equivalent_cost_usd;
|
|
79
|
+
}
|
|
56
80
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
]);
|
|
61
|
-
renderQuotaGauges(document.getElementById('chart-quota'), data, { cost7d: cost7d.api_equivalent_cost_usd });
|
|
81
|
+
renderQuotaGauges(document.getElementById('chart-quota'), data, {
|
|
82
|
+
cost7d: cost7dValue, quotaWindowFrom, quotaWindowTo,
|
|
83
|
+
});
|
|
62
84
|
const el = document.getElementById('quota-last-updated');
|
|
63
85
|
if (el && data.lastFetched) el.textContent = `Updated ${new Date(data.lastFetched).toLocaleTimeString()} ${getTimezoneAbbr()}`;
|
|
64
86
|
} catch { /* silently degrade */ }
|
|
@@ -265,9 +287,18 @@ function init() {
|
|
|
265
287
|
}
|
|
266
288
|
}).catch(() => {});
|
|
267
289
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
290
|
+
// Default date range to the 7-day quota window (resets_at - 7 days → resets_at)
|
|
291
|
+
fetchQuota().then(data => {
|
|
292
|
+
const window = getQuotaWindow(data.seven_day);
|
|
293
|
+
if (window) {
|
|
294
|
+
const fmt = d => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
|
|
295
|
+
datePicker.setRange(fmt(window.from), fmt(window.to));
|
|
296
|
+
}
|
|
297
|
+
}).catch(() => {}).finally(() => {
|
|
298
|
+
loadAll();
|
|
299
|
+
loadQuota();
|
|
300
|
+
startAutoRefresh();
|
|
301
|
+
});
|
|
271
302
|
}
|
|
272
303
|
|
|
273
304
|
document.addEventListener('DOMContentLoaded', init);
|
|
@@ -95,9 +95,18 @@ export function renderQuotaGauges(container, data, opts = {}) {
|
|
|
95
95
|
const proj = document.createElement('div');
|
|
96
96
|
proj.style.cssText = 'margin-top:12px;padding:8px 12px;background:#1e293b;border-radius:6px;font-size:12px;color:#94a3b8';
|
|
97
97
|
const monthlyProjected = projectedCost * (30 / 7);
|
|
98
|
+
|
|
99
|
+
// Format the quota window range for display
|
|
100
|
+
const dtFmt = { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' };
|
|
101
|
+
const tzParts = new Intl.DateTimeFormat(undefined, { timeZoneName: 'short' }).formatToParts(new Date());
|
|
102
|
+
const tz = tzParts.find(p => p.type === 'timeZoneName')?.value || '';
|
|
103
|
+
const windowRange = opts.quotaWindowFrom && opts.quotaWindowTo
|
|
104
|
+
? ` <span style="color:#64748b">(${opts.quotaWindowFrom.toLocaleString(undefined, dtFmt)} → ${opts.quotaWindowTo.toLocaleString(undefined, dtFmt)} ${tz})</span>`
|
|
105
|
+
: '';
|
|
106
|
+
|
|
98
107
|
proj.innerHTML =
|
|
99
|
-
`7-day usage: <strong style="color:#e2e8f0">$${opts.cost7d.toFixed(2)}</strong> API cost at <strong style="color:#e2e8f0">${sevenDay.utilization.toFixed(1)}%</strong> quota` +
|
|
100
|
-
|
|
108
|
+
`7-day usage: <strong style="color:#e2e8f0">$${opts.cost7d.toFixed(2)}</strong> API cost at <strong style="color:#e2e8f0">${sevenDay.utilization.toFixed(1)}%</strong> quota${windowRange}` +
|
|
109
|
+
`<br>Projected at 100%: <strong style="color:#fbbf24">$${projectedCost.toFixed(2)}</strong>/week` +
|
|
101
110
|
` · <strong style="color:#fbbf24">$${monthlyProjected.toFixed(2)}</strong>/month`;
|
|
102
111
|
container.appendChild(proj);
|
|
103
112
|
}
|
package/server/aggregator.js
CHANGED
|
@@ -2,9 +2,13 @@ import { calculateRecordCost, getModelPricing } from './pricing.js';
|
|
|
2
2
|
|
|
3
3
|
export function filterByDateRange(records, from, to) {
|
|
4
4
|
if (!from && !to) return records;
|
|
5
|
-
//
|
|
6
|
-
const start = from
|
|
7
|
-
|
|
5
|
+
// Support both date-only (YYYY-MM-DD → local time boundaries) and full ISO timestamps
|
|
6
|
+
const start = from
|
|
7
|
+
? new Date(from.includes('T') ? from : from + 'T00:00:00.000').getTime()
|
|
8
|
+
: -Infinity;
|
|
9
|
+
const end = to
|
|
10
|
+
? new Date(to.includes('T') ? to : to + 'T23:59:59.999').getTime()
|
|
11
|
+
: Infinity;
|
|
8
12
|
return records.filter(r => {
|
|
9
13
|
const t = new Date(r.timestamp).getTime();
|
|
10
14
|
return t >= start && t <= end;
|