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 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). 7-day reset shows full date+time; all timestamps include timezone
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 { pathToFileURL } = require('url');
5
-
6
- const serverPath = join(__dirname, '..', 'server', 'index.js');
7
- import(pathToFileURL(serverPath).href);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-dashboard",
3
- "version": "1.3.11",
3
+ "version": "1.3.13",
4
4
  "description": "Dashboard that visualizes Claude Code usage from local session logs",
5
5
  "main": "server/index.js",
6
6
  "bin": {
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 today = new Date();
53
- const sevenDaysAgo = new Date(today);
54
- sevenDaysAgo.setDate(today.getDate() - 7);
55
- const fmt = d => d.toISOString().slice(0, 10);
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
- const [data, cost7d] = await Promise.all([
58
- fetchQuota(),
59
- fetchCost({ from: fmt(sevenDaysAgo), to: fmt(today), plan: state.plan.plan }),
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
- loadAll();
269
- loadQuota();
270
- startAutoRefresh();
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
- ` · Projected at 100%: <strong style="color:#fbbf24">$${projectedCost.toFixed(2)}</strong>/week` +
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
  }
@@ -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
- // Use local time boundaries (no Z suffix = local timezone)
6
- const start = from ? new Date(from + 'T00:00:00.000').getTime() : -Infinity;
7
- const end = to ? new Date(to + 'T23:59:59.999').getTime() : Infinity;
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;