claude-usage-dashboard 1.3.13 → 1.4.0
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 +32 -22
- package/bin/cli.cjs +13 -1
- package/bin/cli.js +16 -2
- package/bin/cli.sh +11 -11
- package/package.json +43 -40
- package/public/css/style.css +265 -265
- package/public/index.html +108 -108
- package/public/js/api.js +16 -16
- package/public/js/app.js +304 -304
- package/public/js/charts/cache-efficiency.js +29 -29
- package/public/js/charts/cost-comparison.js +39 -39
- package/public/js/charts/model-distribution.js +56 -56
- package/public/js/charts/project-distribution.js +103 -103
- package/public/js/charts/session-stats.js +117 -117
- package/public/js/charts/token-trend.js +357 -357
- package/public/js/components/date-picker.js +35 -35
- package/public/js/components/plan-selector.js +57 -57
- package/server/aggregator.js +151 -151
- package/server/credentials.js +112 -112
- package/server/index.js +45 -33
- package/server/parser.js +129 -109
- package/server/pricing.js +52 -52
- package/server/routes/api.js +130 -127
- package/server/sync.js +69 -0
package/public/js/app.js
CHANGED
|
@@ -1,304 +1,304 @@
|
|
|
1
|
-
import { fetchUsage, fetchModels, fetchProjects, fetchSessions, fetchCost, fetchCache, fetchStatus, fetchQuota, fetchSubscription } from './api.js';
|
|
2
|
-
import { initDatePicker } from './components/date-picker.js';
|
|
3
|
-
import { initPlanSelector } from './components/plan-selector.js';
|
|
4
|
-
import { renderTokenTrend } from './charts/token-trend.js';
|
|
5
|
-
import { renderCostComparison } from './charts/cost-comparison.js';
|
|
6
|
-
import { renderModelDistribution } from './charts/model-distribution.js';
|
|
7
|
-
import { renderCacheEfficiency } from './charts/cache-efficiency.js';
|
|
8
|
-
import { renderProjectDistribution } from './charts/project-distribution.js';
|
|
9
|
-
import { renderSessionTable } from './charts/session-stats.js';
|
|
10
|
-
import { renderQuotaGauges } from './charts/quota-gauge.js';
|
|
11
|
-
|
|
12
|
-
const state = {
|
|
13
|
-
dateRange: { from: null, to: null },
|
|
14
|
-
plan: { plan: 'max20x', customPrice: null },
|
|
15
|
-
granularity: localStorage.getItem('selectedGranularity') || 'hourly',
|
|
16
|
-
trendYAxis: localStorage.getItem('trendYAxis') || 'tokens',
|
|
17
|
-
sessionSort: 'date',
|
|
18
|
-
sessionOrder: 'desc',
|
|
19
|
-
sessionPage: 1,
|
|
20
|
-
sessionProject: '',
|
|
21
|
-
autoRefresh: localStorage.getItem('autoRefresh') !== 'false',
|
|
22
|
-
autoRefreshInterval: 30,
|
|
23
|
-
_refreshTimer: null,
|
|
24
|
-
quotaRefreshInterval: 120,
|
|
25
|
-
_quotaTimer: null,
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
let datePicker, planSelector;
|
|
29
|
-
|
|
30
|
-
function formatNumber(n) {
|
|
31
|
-
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
|
|
32
|
-
if (n >= 1_000) return (n / 1_000).toFixed(0) + 'K';
|
|
33
|
-
return n.toString();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function updateLastUpdated() {
|
|
37
|
-
const el = document.getElementById('last-updated');
|
|
38
|
-
if (el) {
|
|
39
|
-
const now = new Date();
|
|
40
|
-
el.textContent = `Updated ${now.toLocaleTimeString()} ${getTimezoneAbbr()}`;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function getTimezoneAbbr() {
|
|
45
|
-
const parts = new Intl.DateTimeFormat(undefined, { timeZoneName: 'short' }).formatToParts(new Date());
|
|
46
|
-
const tz = parts.find(p => p.type === 'timeZoneName');
|
|
47
|
-
return tz ? tz.value : '';
|
|
48
|
-
}
|
|
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
|
-
|
|
60
|
-
async function loadQuota() {
|
|
61
|
-
try {
|
|
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
|
-
}
|
|
80
|
-
|
|
81
|
-
renderQuotaGauges(document.getElementById('chart-quota'), data, {
|
|
82
|
-
cost7d: cost7dValue, quotaWindowFrom, quotaWindowTo,
|
|
83
|
-
});
|
|
84
|
-
const el = document.getElementById('quota-last-updated');
|
|
85
|
-
if (el && data.lastFetched) el.textContent = `Updated ${new Date(data.lastFetched).toLocaleTimeString()} ${getTimezoneAbbr()}`;
|
|
86
|
-
} catch { /* silently degrade */ }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function startAutoRefresh() {
|
|
90
|
-
stopAutoRefresh();
|
|
91
|
-
if (state.autoRefresh) {
|
|
92
|
-
state._refreshTimer = setInterval(() => loadAll(), state.autoRefreshInterval * 1000);
|
|
93
|
-
state._quotaTimer = setInterval(() => loadQuota(), state.quotaRefreshInterval * 1000);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function stopAutoRefresh() {
|
|
98
|
-
if (state._refreshTimer) {
|
|
99
|
-
clearInterval(state._refreshTimer);
|
|
100
|
-
state._refreshTimer = null;
|
|
101
|
-
}
|
|
102
|
-
if (state._quotaTimer) {
|
|
103
|
-
clearInterval(state._quotaTimer);
|
|
104
|
-
state._quotaTimer = null;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function loadAll() {
|
|
109
|
-
const params = { ...state.dateRange };
|
|
110
|
-
const planParams = { ...state.dateRange, plan: state.plan.plan };
|
|
111
|
-
if (state.plan.customPrice) planParams.customPrice = state.plan.customPrice;
|
|
112
|
-
|
|
113
|
-
const [usage, models, projects, sessions, cost, cache] = await Promise.all([
|
|
114
|
-
fetchUsage({ ...params, granularity: state.granularity }),
|
|
115
|
-
fetchModels(params),
|
|
116
|
-
fetchProjects(params),
|
|
117
|
-
fetchSessions({
|
|
118
|
-
...params,
|
|
119
|
-
project: state.sessionProject,
|
|
120
|
-
sort: state.sessionSort,
|
|
121
|
-
order: state.sessionOrder,
|
|
122
|
-
page: state.sessionPage,
|
|
123
|
-
}),
|
|
124
|
-
fetchCost(planParams),
|
|
125
|
-
fetchCache(params),
|
|
126
|
-
]);
|
|
127
|
-
|
|
128
|
-
// Summary cards
|
|
129
|
-
const t = usage.total;
|
|
130
|
-
const totalAll = t.input_tokens + t.output_tokens + t.cache_read_tokens + t.cache_creation_tokens;
|
|
131
|
-
document.getElementById('val-total-tokens').textContent = formatNumber(totalAll);
|
|
132
|
-
document.getElementById('sub-total-tokens').innerHTML =
|
|
133
|
-
`<span style="color:#4ade80">cache read:${formatNumber(t.cache_read_tokens)}</span> · ` +
|
|
134
|
-
`<span style="color:#f59e0b">cache write:${formatNumber(t.cache_creation_tokens)}</span> · ` +
|
|
135
|
-
`<span style="color:#60a5fa">in:${formatNumber(t.input_tokens)}</span> · ` +
|
|
136
|
-
`<span style="color:#f97316">out:${formatNumber(t.output_tokens)}</span>`;
|
|
137
|
-
document.getElementById('val-api-cost').textContent = `$${cost.api_equivalent_cost_usd.toFixed(2)}`;
|
|
138
|
-
|
|
139
|
-
document.getElementById('val-cache-rate').textContent = `${(cache.cache_read_rate * 100).toFixed(1)}%`;
|
|
140
|
-
|
|
141
|
-
// Set active granularity button
|
|
142
|
-
const activeGran = usage.granularity;
|
|
143
|
-
document.querySelectorAll('#granularity-toggle button').forEach(btn => {
|
|
144
|
-
btn.classList.toggle('active', btn.dataset.granularity === activeGran);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Charts
|
|
148
|
-
renderTokenTrend(document.getElementById('chart-token-trend'), usage, { yAxis: state.trendYAxis });
|
|
149
|
-
renderCostComparison(document.getElementById('chart-cost-comparison'), cost);
|
|
150
|
-
renderModelDistribution(document.getElementById('chart-model-distribution'), models);
|
|
151
|
-
renderCacheEfficiency(document.getElementById('chart-cache-efficiency'), cache);
|
|
152
|
-
renderProjectDistribution(document.getElementById('chart-project-distribution'), projects);
|
|
153
|
-
renderSessionTable(document.getElementById('session-table'), sessions, {
|
|
154
|
-
onSort: (key) => {
|
|
155
|
-
if (state.sessionSort === key) {
|
|
156
|
-
state.sessionOrder = state.sessionOrder === 'desc' ? 'asc' : 'desc';
|
|
157
|
-
} else {
|
|
158
|
-
state.sessionSort = key;
|
|
159
|
-
state.sessionOrder = 'desc';
|
|
160
|
-
}
|
|
161
|
-
state.sessionPage = 1;
|
|
162
|
-
loadAll();
|
|
163
|
-
},
|
|
164
|
-
onPageChange: (page) => {
|
|
165
|
-
state.sessionPage = page;
|
|
166
|
-
loadAll();
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
updateLastUpdated();
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Max bucket limits per granularity to avoid crashing the browser
|
|
174
|
-
const GRANULARITY_MAX_DAYS = { hourly: 14, daily: 90, weekly: 365, monthly: 1825 };
|
|
175
|
-
|
|
176
|
-
function updateGranularityButtons() {
|
|
177
|
-
const { from, to } = state.dateRange;
|
|
178
|
-
const days = (from && to) ? (new Date(to) - new Date(from)) / (1000 * 60 * 60 * 24) : 30;
|
|
179
|
-
document.querySelectorAll('#granularity-toggle button').forEach(btn => {
|
|
180
|
-
const gran = btn.dataset.granularity;
|
|
181
|
-
const maxDays = GRANULARITY_MAX_DAYS[gran] || 9999;
|
|
182
|
-
const tooLarge = days > maxDays;
|
|
183
|
-
btn.disabled = tooLarge;
|
|
184
|
-
btn.title = tooLarge ? `Range too large for ${gran} view (max ${maxDays} days)` : '';
|
|
185
|
-
});
|
|
186
|
-
// If currently selected granularity is now disabled, switch to the finest available
|
|
187
|
-
const currentBtn = document.querySelector(`#granularity-toggle button[data-granularity="${state.granularity}"]`);
|
|
188
|
-
if (currentBtn && currentBtn.disabled) {
|
|
189
|
-
const order = ['hourly', 'daily', 'weekly', 'monthly'];
|
|
190
|
-
const available = order.find(g => {
|
|
191
|
-
const b = document.querySelector(`#granularity-toggle button[data-granularity="${g}"]`);
|
|
192
|
-
return b && !b.disabled;
|
|
193
|
-
});
|
|
194
|
-
if (available) {
|
|
195
|
-
state.granularity = available;
|
|
196
|
-
localStorage.setItem('selectedGranularity', state.granularity);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// Update active class
|
|
200
|
-
document.querySelectorAll('#granularity-toggle button').forEach(btn => {
|
|
201
|
-
btn.classList.toggle('active', btn.dataset.granularity === state.granularity);
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function init() {
|
|
206
|
-
datePicker = initDatePicker(document.getElementById('date-picker'), (range) => {
|
|
207
|
-
state.dateRange = range;
|
|
208
|
-
state.sessionPage = 1;
|
|
209
|
-
updateGranularityButtons();
|
|
210
|
-
loadAll();
|
|
211
|
-
});
|
|
212
|
-
state.dateRange = datePicker.getRange();
|
|
213
|
-
updateGranularityButtons();
|
|
214
|
-
|
|
215
|
-
planSelector = initPlanSelector(document.getElementById('plan-selector'), (plan) => {
|
|
216
|
-
state.plan = plan;
|
|
217
|
-
loadAll();
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
document.getElementById('granularity-toggle').addEventListener('click', (e) => {
|
|
221
|
-
if (e.target.tagName === 'BUTTON' && !e.target.disabled) {
|
|
222
|
-
state.granularity = e.target.dataset.granularity;
|
|
223
|
-
localStorage.setItem('selectedGranularity', state.granularity);
|
|
224
|
-
loadAll();
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// Y-axis toggle (tokens / dollars)
|
|
229
|
-
const yaxisToggle = document.getElementById('yaxis-toggle');
|
|
230
|
-
yaxisToggle.querySelectorAll('button').forEach(btn => {
|
|
231
|
-
btn.classList.toggle('active', btn.dataset.yaxis === state.trendYAxis);
|
|
232
|
-
});
|
|
233
|
-
yaxisToggle.addEventListener('click', (e) => {
|
|
234
|
-
if (e.target.tagName === 'BUTTON') {
|
|
235
|
-
state.trendYAxis = e.target.dataset.yaxis;
|
|
236
|
-
localStorage.setItem('trendYAxis', state.trendYAxis);
|
|
237
|
-
yaxisToggle.querySelectorAll('button').forEach(btn => {
|
|
238
|
-
btn.classList.toggle('active', btn.dataset.yaxis === state.trendYAxis);
|
|
239
|
-
});
|
|
240
|
-
loadAll();
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const filterInput = document.getElementById('session-filter');
|
|
245
|
-
let filterTimeout;
|
|
246
|
-
filterInput.addEventListener('input', () => {
|
|
247
|
-
clearTimeout(filterTimeout);
|
|
248
|
-
filterTimeout = setTimeout(() => {
|
|
249
|
-
state.sessionProject = filterInput.value.trim();
|
|
250
|
-
state.sessionPage = 1;
|
|
251
|
-
loadAll();
|
|
252
|
-
}, 300);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
document.getElementById('session-sort').addEventListener('change', (e) => {
|
|
256
|
-
state.sessionSort = e.target.value;
|
|
257
|
-
state.sessionOrder = 'desc';
|
|
258
|
-
state.sessionPage = 1;
|
|
259
|
-
loadAll();
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
document.getElementById('btn-refresh').addEventListener('click', () => { loadAll(); loadQuota(); });
|
|
263
|
-
|
|
264
|
-
const autoToggle = document.getElementById('auto-refresh-toggle');
|
|
265
|
-
autoToggle.checked = state.autoRefresh;
|
|
266
|
-
autoToggle.addEventListener('change', () => {
|
|
267
|
-
state.autoRefresh = autoToggle.checked;
|
|
268
|
-
localStorage.setItem('autoRefresh', state.autoRefresh);
|
|
269
|
-
if (state.autoRefresh) {
|
|
270
|
-
startAutoRefresh();
|
|
271
|
-
} else {
|
|
272
|
-
stopAutoRefresh();
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
// Auto-detect subscription tier
|
|
277
|
-
fetchSubscription().then(info => {
|
|
278
|
-
if (info.plan) {
|
|
279
|
-
planSelector.setDetectedPlan(info.plan);
|
|
280
|
-
state.plan = planSelector.getPlan();
|
|
281
|
-
}
|
|
282
|
-
const tierLabels = { pro: 'Pro', max5x: 'Max 5x', max20x: 'Max 20x' };
|
|
283
|
-
const label = tierLabels[info.plan];
|
|
284
|
-
if (label) {
|
|
285
|
-
const h2 = document.querySelector('#quota-section h2');
|
|
286
|
-
if (h2) h2.textContent = `Subscription Quota (${label})`;
|
|
287
|
-
}
|
|
288
|
-
}).catch(() => {});
|
|
289
|
-
|
|
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
|
-
});
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
document.addEventListener('DOMContentLoaded', init);
|
|
1
|
+
import { fetchUsage, fetchModels, fetchProjects, fetchSessions, fetchCost, fetchCache, fetchStatus, fetchQuota, fetchSubscription } from './api.js';
|
|
2
|
+
import { initDatePicker } from './components/date-picker.js';
|
|
3
|
+
import { initPlanSelector } from './components/plan-selector.js';
|
|
4
|
+
import { renderTokenTrend } from './charts/token-trend.js';
|
|
5
|
+
import { renderCostComparison } from './charts/cost-comparison.js';
|
|
6
|
+
import { renderModelDistribution } from './charts/model-distribution.js';
|
|
7
|
+
import { renderCacheEfficiency } from './charts/cache-efficiency.js';
|
|
8
|
+
import { renderProjectDistribution } from './charts/project-distribution.js';
|
|
9
|
+
import { renderSessionTable } from './charts/session-stats.js';
|
|
10
|
+
import { renderQuotaGauges } from './charts/quota-gauge.js';
|
|
11
|
+
|
|
12
|
+
const state = {
|
|
13
|
+
dateRange: { from: null, to: null },
|
|
14
|
+
plan: { plan: 'max20x', customPrice: null },
|
|
15
|
+
granularity: localStorage.getItem('selectedGranularity') || 'hourly',
|
|
16
|
+
trendYAxis: localStorage.getItem('trendYAxis') || 'tokens',
|
|
17
|
+
sessionSort: 'date',
|
|
18
|
+
sessionOrder: 'desc',
|
|
19
|
+
sessionPage: 1,
|
|
20
|
+
sessionProject: '',
|
|
21
|
+
autoRefresh: localStorage.getItem('autoRefresh') !== 'false',
|
|
22
|
+
autoRefreshInterval: 30,
|
|
23
|
+
_refreshTimer: null,
|
|
24
|
+
quotaRefreshInterval: 120,
|
|
25
|
+
_quotaTimer: null,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let datePicker, planSelector;
|
|
29
|
+
|
|
30
|
+
function formatNumber(n) {
|
|
31
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
|
|
32
|
+
if (n >= 1_000) return (n / 1_000).toFixed(0) + 'K';
|
|
33
|
+
return n.toString();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function updateLastUpdated() {
|
|
37
|
+
const el = document.getElementById('last-updated');
|
|
38
|
+
if (el) {
|
|
39
|
+
const now = new Date();
|
|
40
|
+
el.textContent = `Updated ${now.toLocaleTimeString()} ${getTimezoneAbbr()}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getTimezoneAbbr() {
|
|
45
|
+
const parts = new Intl.DateTimeFormat(undefined, { timeZoneName: 'short' }).formatToParts(new Date());
|
|
46
|
+
const tz = parts.find(p => p.type === 'timeZoneName');
|
|
47
|
+
return tz ? tz.value : '';
|
|
48
|
+
}
|
|
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
|
+
|
|
60
|
+
async function loadQuota() {
|
|
61
|
+
try {
|
|
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
|
+
}
|
|
80
|
+
|
|
81
|
+
renderQuotaGauges(document.getElementById('chart-quota'), data, {
|
|
82
|
+
cost7d: cost7dValue, quotaWindowFrom, quotaWindowTo,
|
|
83
|
+
});
|
|
84
|
+
const el = document.getElementById('quota-last-updated');
|
|
85
|
+
if (el && data.lastFetched) el.textContent = `Updated ${new Date(data.lastFetched).toLocaleTimeString()} ${getTimezoneAbbr()}`;
|
|
86
|
+
} catch { /* silently degrade */ }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function startAutoRefresh() {
|
|
90
|
+
stopAutoRefresh();
|
|
91
|
+
if (state.autoRefresh) {
|
|
92
|
+
state._refreshTimer = setInterval(() => loadAll(), state.autoRefreshInterval * 1000);
|
|
93
|
+
state._quotaTimer = setInterval(() => loadQuota(), state.quotaRefreshInterval * 1000);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function stopAutoRefresh() {
|
|
98
|
+
if (state._refreshTimer) {
|
|
99
|
+
clearInterval(state._refreshTimer);
|
|
100
|
+
state._refreshTimer = null;
|
|
101
|
+
}
|
|
102
|
+
if (state._quotaTimer) {
|
|
103
|
+
clearInterval(state._quotaTimer);
|
|
104
|
+
state._quotaTimer = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function loadAll() {
|
|
109
|
+
const params = { ...state.dateRange };
|
|
110
|
+
const planParams = { ...state.dateRange, plan: state.plan.plan };
|
|
111
|
+
if (state.plan.customPrice) planParams.customPrice = state.plan.customPrice;
|
|
112
|
+
|
|
113
|
+
const [usage, models, projects, sessions, cost, cache] = await Promise.all([
|
|
114
|
+
fetchUsage({ ...params, granularity: state.granularity }),
|
|
115
|
+
fetchModels(params),
|
|
116
|
+
fetchProjects(params),
|
|
117
|
+
fetchSessions({
|
|
118
|
+
...params,
|
|
119
|
+
project: state.sessionProject,
|
|
120
|
+
sort: state.sessionSort,
|
|
121
|
+
order: state.sessionOrder,
|
|
122
|
+
page: state.sessionPage,
|
|
123
|
+
}),
|
|
124
|
+
fetchCost(planParams),
|
|
125
|
+
fetchCache(params),
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
// Summary cards
|
|
129
|
+
const t = usage.total;
|
|
130
|
+
const totalAll = t.input_tokens + t.output_tokens + t.cache_read_tokens + t.cache_creation_tokens;
|
|
131
|
+
document.getElementById('val-total-tokens').textContent = formatNumber(totalAll);
|
|
132
|
+
document.getElementById('sub-total-tokens').innerHTML =
|
|
133
|
+
`<span style="color:#4ade80">cache read:${formatNumber(t.cache_read_tokens)}</span> · ` +
|
|
134
|
+
`<span style="color:#f59e0b">cache write:${formatNumber(t.cache_creation_tokens)}</span> · ` +
|
|
135
|
+
`<span style="color:#60a5fa">in:${formatNumber(t.input_tokens)}</span> · ` +
|
|
136
|
+
`<span style="color:#f97316">out:${formatNumber(t.output_tokens)}</span>`;
|
|
137
|
+
document.getElementById('val-api-cost').textContent = `$${cost.api_equivalent_cost_usd.toFixed(2)}`;
|
|
138
|
+
|
|
139
|
+
document.getElementById('val-cache-rate').textContent = `${(cache.cache_read_rate * 100).toFixed(1)}%`;
|
|
140
|
+
|
|
141
|
+
// Set active granularity button
|
|
142
|
+
const activeGran = usage.granularity;
|
|
143
|
+
document.querySelectorAll('#granularity-toggle button').forEach(btn => {
|
|
144
|
+
btn.classList.toggle('active', btn.dataset.granularity === activeGran);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Charts
|
|
148
|
+
renderTokenTrend(document.getElementById('chart-token-trend'), usage, { yAxis: state.trendYAxis });
|
|
149
|
+
renderCostComparison(document.getElementById('chart-cost-comparison'), cost);
|
|
150
|
+
renderModelDistribution(document.getElementById('chart-model-distribution'), models);
|
|
151
|
+
renderCacheEfficiency(document.getElementById('chart-cache-efficiency'), cache);
|
|
152
|
+
renderProjectDistribution(document.getElementById('chart-project-distribution'), projects);
|
|
153
|
+
renderSessionTable(document.getElementById('session-table'), sessions, {
|
|
154
|
+
onSort: (key) => {
|
|
155
|
+
if (state.sessionSort === key) {
|
|
156
|
+
state.sessionOrder = state.sessionOrder === 'desc' ? 'asc' : 'desc';
|
|
157
|
+
} else {
|
|
158
|
+
state.sessionSort = key;
|
|
159
|
+
state.sessionOrder = 'desc';
|
|
160
|
+
}
|
|
161
|
+
state.sessionPage = 1;
|
|
162
|
+
loadAll();
|
|
163
|
+
},
|
|
164
|
+
onPageChange: (page) => {
|
|
165
|
+
state.sessionPage = page;
|
|
166
|
+
loadAll();
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
updateLastUpdated();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Max bucket limits per granularity to avoid crashing the browser
|
|
174
|
+
const GRANULARITY_MAX_DAYS = { hourly: 14, daily: 90, weekly: 365, monthly: 1825 };
|
|
175
|
+
|
|
176
|
+
function updateGranularityButtons() {
|
|
177
|
+
const { from, to } = state.dateRange;
|
|
178
|
+
const days = (from && to) ? (new Date(to) - new Date(from)) / (1000 * 60 * 60 * 24) : 30;
|
|
179
|
+
document.querySelectorAll('#granularity-toggle button').forEach(btn => {
|
|
180
|
+
const gran = btn.dataset.granularity;
|
|
181
|
+
const maxDays = GRANULARITY_MAX_DAYS[gran] || 9999;
|
|
182
|
+
const tooLarge = days > maxDays;
|
|
183
|
+
btn.disabled = tooLarge;
|
|
184
|
+
btn.title = tooLarge ? `Range too large for ${gran} view (max ${maxDays} days)` : '';
|
|
185
|
+
});
|
|
186
|
+
// If currently selected granularity is now disabled, switch to the finest available
|
|
187
|
+
const currentBtn = document.querySelector(`#granularity-toggle button[data-granularity="${state.granularity}"]`);
|
|
188
|
+
if (currentBtn && currentBtn.disabled) {
|
|
189
|
+
const order = ['hourly', 'daily', 'weekly', 'monthly'];
|
|
190
|
+
const available = order.find(g => {
|
|
191
|
+
const b = document.querySelector(`#granularity-toggle button[data-granularity="${g}"]`);
|
|
192
|
+
return b && !b.disabled;
|
|
193
|
+
});
|
|
194
|
+
if (available) {
|
|
195
|
+
state.granularity = available;
|
|
196
|
+
localStorage.setItem('selectedGranularity', state.granularity);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Update active class
|
|
200
|
+
document.querySelectorAll('#granularity-toggle button').forEach(btn => {
|
|
201
|
+
btn.classList.toggle('active', btn.dataset.granularity === state.granularity);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function init() {
|
|
206
|
+
datePicker = initDatePicker(document.getElementById('date-picker'), (range) => {
|
|
207
|
+
state.dateRange = range;
|
|
208
|
+
state.sessionPage = 1;
|
|
209
|
+
updateGranularityButtons();
|
|
210
|
+
loadAll();
|
|
211
|
+
});
|
|
212
|
+
state.dateRange = datePicker.getRange();
|
|
213
|
+
updateGranularityButtons();
|
|
214
|
+
|
|
215
|
+
planSelector = initPlanSelector(document.getElementById('plan-selector'), (plan) => {
|
|
216
|
+
state.plan = plan;
|
|
217
|
+
loadAll();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
document.getElementById('granularity-toggle').addEventListener('click', (e) => {
|
|
221
|
+
if (e.target.tagName === 'BUTTON' && !e.target.disabled) {
|
|
222
|
+
state.granularity = e.target.dataset.granularity;
|
|
223
|
+
localStorage.setItem('selectedGranularity', state.granularity);
|
|
224
|
+
loadAll();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Y-axis toggle (tokens / dollars)
|
|
229
|
+
const yaxisToggle = document.getElementById('yaxis-toggle');
|
|
230
|
+
yaxisToggle.querySelectorAll('button').forEach(btn => {
|
|
231
|
+
btn.classList.toggle('active', btn.dataset.yaxis === state.trendYAxis);
|
|
232
|
+
});
|
|
233
|
+
yaxisToggle.addEventListener('click', (e) => {
|
|
234
|
+
if (e.target.tagName === 'BUTTON') {
|
|
235
|
+
state.trendYAxis = e.target.dataset.yaxis;
|
|
236
|
+
localStorage.setItem('trendYAxis', state.trendYAxis);
|
|
237
|
+
yaxisToggle.querySelectorAll('button').forEach(btn => {
|
|
238
|
+
btn.classList.toggle('active', btn.dataset.yaxis === state.trendYAxis);
|
|
239
|
+
});
|
|
240
|
+
loadAll();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const filterInput = document.getElementById('session-filter');
|
|
245
|
+
let filterTimeout;
|
|
246
|
+
filterInput.addEventListener('input', () => {
|
|
247
|
+
clearTimeout(filterTimeout);
|
|
248
|
+
filterTimeout = setTimeout(() => {
|
|
249
|
+
state.sessionProject = filterInput.value.trim();
|
|
250
|
+
state.sessionPage = 1;
|
|
251
|
+
loadAll();
|
|
252
|
+
}, 300);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
document.getElementById('session-sort').addEventListener('change', (e) => {
|
|
256
|
+
state.sessionSort = e.target.value;
|
|
257
|
+
state.sessionOrder = 'desc';
|
|
258
|
+
state.sessionPage = 1;
|
|
259
|
+
loadAll();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
document.getElementById('btn-refresh').addEventListener('click', () => { loadAll(); loadQuota(); });
|
|
263
|
+
|
|
264
|
+
const autoToggle = document.getElementById('auto-refresh-toggle');
|
|
265
|
+
autoToggle.checked = state.autoRefresh;
|
|
266
|
+
autoToggle.addEventListener('change', () => {
|
|
267
|
+
state.autoRefresh = autoToggle.checked;
|
|
268
|
+
localStorage.setItem('autoRefresh', state.autoRefresh);
|
|
269
|
+
if (state.autoRefresh) {
|
|
270
|
+
startAutoRefresh();
|
|
271
|
+
} else {
|
|
272
|
+
stopAutoRefresh();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Auto-detect subscription tier
|
|
277
|
+
fetchSubscription().then(info => {
|
|
278
|
+
if (info.plan) {
|
|
279
|
+
planSelector.setDetectedPlan(info.plan);
|
|
280
|
+
state.plan = planSelector.getPlan();
|
|
281
|
+
}
|
|
282
|
+
const tierLabels = { pro: 'Pro', max5x: 'Max 5x', max20x: 'Max 20x' };
|
|
283
|
+
const label = tierLabels[info.plan];
|
|
284
|
+
if (label) {
|
|
285
|
+
const h2 = document.querySelector('#quota-section h2');
|
|
286
|
+
if (h2) h2.textContent = `Subscription Quota (${label})`;
|
|
287
|
+
}
|
|
288
|
+
}).catch(() => {});
|
|
289
|
+
|
|
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
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
document.addEventListener('DOMContentLoaded', init);
|