claude-usage-dashboard 1.5.6 → 1.5.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-dashboard",
3
- "version": "1.5.6",
3
+ "version": "1.5.8",
4
4
  "description": "Claude Code usage dashboard — token costs, quota cycle tracking, cache efficiency, multi-machine sync across all your devices",
5
5
  "main": "server/index.js",
6
6
  "bin": {
package/public/js/app.js CHANGED
@@ -30,32 +30,6 @@ const state = {
30
30
  let datePicker, planSelector;
31
31
  let _cachedCycleData = null;
32
32
 
33
- /**
34
- * Find a quota cycle whose period matches the selected date range.
35
- * Returns the cycle's overall metrics, or null if no match.
36
- * Tolerance: 25 hours (handles hour-level normalization and timezone offsets).
37
- */
38
- function findMatchingCycle(dateRange, cycleData) {
39
- if (!dateRange.from || !dateRange.to || !cycleData) return null;
40
- const viewFrom = new Date(dateRange.from).getTime();
41
- const viewTo = new Date(dateRange.to).getTime();
42
- const tolerance = 25 * 60 * 60 * 1000;
43
-
44
- const candidates = [];
45
- if (cycleData.currentCycle) candidates.push(cycleData.currentCycle);
46
- if (cycleData.history) candidates.push(...cycleData.history);
47
-
48
- for (const c of candidates) {
49
- if (!c.start || !c.resets_at || !c.overall?.tokens) continue;
50
- const cStart = new Date(c.start).getTime();
51
- const cEnd = new Date(c.resets_at).getTime();
52
- if (Math.abs(viewFrom - cStart) < tolerance && Math.abs(viewTo - cEnd) < tolerance) {
53
- return c.overall;
54
- }
55
- }
56
- return null;
57
- }
58
-
59
33
  function formatNumber(n) {
60
34
  if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
61
35
  if (n >= 1_000) return (n / 1_000).toFixed(0) + 'K';
@@ -100,16 +74,12 @@ async function loadQuota() {
100
74
  if (window && sevenDay.utilization > 0) {
101
75
  quotaWindowFrom = window.from;
102
76
  quotaWindowTo = window.to;
103
- // Prefer cycle data (multi-machine merged) over cost API (local only)
104
- cost7dValue = cycleData?.currentCycle?.overall?.actualCost || 0;
105
- if (cost7dValue === 0) {
106
- const cost7d = await fetchCost({
107
- from: window.from.toISOString(),
108
- to: window.to.toISOString(),
109
- plan: state.plan.plan,
110
- });
111
- cost7dValue = cost7d.api_equivalent_cost_usd;
112
- }
77
+ const cost7d = await fetchCost({
78
+ from: window.from.toISOString(),
79
+ to: window.to.toISOString(),
80
+ plan: state.plan.plan,
81
+ });
82
+ cost7dValue = cost7d.api_equivalent_cost_usd;
113
83
  }
114
84
 
115
85
  renderQuotaGauges(document.getElementById('chart-quota'), data, {
@@ -170,30 +140,18 @@ async function loadAll() {
170
140
  fetchCache(params),
171
141
  ]);
172
142
 
173
- // Summary cards — use multi-machine cycle data when date range matches a cycle
143
+ // Summary cards
174
144
  const t = usage.total;
175
- let tokIn = t.input_tokens, tokOut = t.output_tokens;
176
- let tokCR = t.cache_read_tokens, tokCW = t.cache_creation_tokens;
177
- let apiCost = cost.api_equivalent_cost_usd;
178
- const matchedCycle = findMatchingCycle(state.dateRange, _cachedCycleData);
179
- if (matchedCycle?.tokens) {
180
- tokIn = matchedCycle.tokens.input; tokOut = matchedCycle.tokens.output;
181
- tokCR = matchedCycle.tokens.cacheRead; tokCW = matchedCycle.tokens.cacheCreation;
182
- apiCost = matchedCycle.actualCost;
183
- }
184
- const totalAll = tokIn + tokOut + tokCR + tokCW;
145
+ const totalAll = t.input_tokens + t.output_tokens + t.cache_read_tokens + t.cache_creation_tokens;
185
146
  document.getElementById('val-total-tokens').textContent = formatNumber(totalAll);
186
147
  document.getElementById('sub-total-tokens').innerHTML =
187
- `<span style="color:#4ade80">cache read:${formatNumber(tokCR)}</span> · ` +
188
- `<span style="color:#f59e0b">cache write:${formatNumber(tokCW)}</span> · ` +
189
- `<span style="color:#60a5fa">in:${formatNumber(tokIn)}</span> · ` +
190
- `<span style="color:#f97316">out:${formatNumber(tokOut)}</span>`;
191
- document.getElementById('val-api-cost').textContent = `$${apiCost.toFixed(2)}`;
192
-
193
- const totalInput = tokIn + tokCR + tokCW;
194
- document.getElementById('val-cache-rate').textContent = totalInput > 0
195
- ? `${((tokCR / totalInput) * 100).toFixed(1)}%`
196
- : `${(cache.cache_read_rate * 100).toFixed(1)}%`;
148
+ `<span style="color:#4ade80">cache read:${formatNumber(t.cache_read_tokens)}</span> · ` +
149
+ `<span style="color:#f59e0b">cache write:${formatNumber(t.cache_creation_tokens)}</span> · ` +
150
+ `<span style="color:#60a5fa">in:${formatNumber(t.input_tokens)}</span> · ` +
151
+ `<span style="color:#f97316">out:${formatNumber(t.output_tokens)}</span>`;
152
+ document.getElementById('val-api-cost').textContent = `$${cost.api_equivalent_cost_usd.toFixed(2)}`;
153
+
154
+ document.getElementById('val-cache-rate').textContent = `${(cache.cache_read_rate * 100).toFixed(1)}%`;
197
155
 
198
156
  // Set active granularity button
199
157
  const activeGran = usage.granularity;
@@ -174,6 +174,11 @@ export function updateQuotaCycleSnapshot(quotaData, logBaseDir, machineName, sna
174
174
  ...cycleData,
175
175
  };
176
176
 
177
+ // Remove stale history entries for the current cycle's period — these are
178
+ // artifacts from past false cycle-switch detections on this same machine.
179
+ const currentKey = cyclePeriodKey(snapshot.currentCycle);
180
+ snapshot.history = snapshot.history.filter(h => cyclePeriodKey(h) !== currentKey);
181
+
177
182
  fs.writeFileSync(filePath, JSON.stringify(snapshot, null, 2));
178
183
  }
179
184
 
@@ -6,7 +6,7 @@ import { filterByDateRange, autoGranularity, aggregateByTime, aggregateBySession
6
6
  import { calculateRecordCost, PLAN_DEFAULTS } from '../pricing.js';
7
7
  import { createQuotaFetcher } from '../quota.js';
8
8
  import { getSubscriptionInfo } from '../credentials.js';
9
- import { updateQuotaCycleSnapshot, loadQuotaCycles } from '../quota-cycles.js';
9
+ import { updateQuotaCycleSnapshot, loadQuotaCycles, computeCycleData } from '../quota-cycles.js';
10
10
 
11
11
  export function createApiRouter(logBaseDir, options = {}) {
12
12
  const router = Router();
@@ -146,6 +146,21 @@ export function createApiRouter(logBaseDir, options = {}) {
146
146
  options.snapshotDir
147
147
  );
148
148
  if (data.currentCycle) {
149
+ // Recompute current cycle from parsed records — in sync mode this
150
+ // includes all machines' data, matching /api/cost and /api/usage.
151
+ // The snapshot's utilization % (from the quota API) is preserved.
152
+ const records = refreshRecords();
153
+ const cycleRecords = filterByDateRange(
154
+ records, data.currentCycle.start, data.currentCycle.resets_at
155
+ );
156
+ const quotaShim = {
157
+ seven_day: { utilization: data.currentCycle.overall.utilization },
158
+ seven_day_opus: { utilization: data.currentCycle.models?.opus?.utilization || 0 },
159
+ seven_day_sonnet: { utilization: data.currentCycle.models?.sonnet?.utilization || 0 },
160
+ };
161
+ const fresh = computeCycleData(cycleRecords, quotaShim);
162
+ Object.assign(data.currentCycle, fresh);
163
+
149
164
  const start = new Date(data.currentCycle.start);
150
165
  const now = new Date();
151
166
  data.currentCycle.daysElapsed = Math.round(((now - start) / (1000 * 60 * 60 * 24)) * 10) / 10;