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 +1 -1
- package/public/js/app.js +15 -57
- package/server/quota-cycles.js +5 -0
- package/server/routes/api.js +16 -1
package/package.json
CHANGED
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
143
|
+
// Summary cards
|
|
174
144
|
const t = usage.total;
|
|
175
|
-
|
|
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(
|
|
188
|
-
`<span style="color:#f59e0b">cache write:${formatNumber(
|
|
189
|
-
`<span style="color:#60a5fa">in:${formatNumber(
|
|
190
|
-
`<span style="color:#f97316">out:${formatNumber(
|
|
191
|
-
document.getElementById('val-api-cost').textContent = `$${
|
|
192
|
-
|
|
193
|
-
|
|
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;
|
package/server/quota-cycles.js
CHANGED
|
@@ -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
|
|
package/server/routes/api.js
CHANGED
|
@@ -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;
|