claude-usage-dashboard 1.3.4 → 1.3.6
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/bin/cli.js +2 -20
- package/package.json +40 -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 +273 -273
- 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 +147 -147
- package/server/index.js +33 -33
- package/server/parser.js +109 -109
- package/server/pricing.js +52 -52
- package/server/routes/api.js +127 -127
package/public/js/app.js
CHANGED
|
@@ -1,273 +1,273 @@
|
|
|
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
|
-
async function loadQuota() {
|
|
51
|
-
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);
|
|
56
|
-
|
|
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 });
|
|
62
|
-
const el = document.getElementById('quota-last-updated');
|
|
63
|
-
if (el && data.lastFetched) el.textContent = `Updated ${new Date(data.lastFetched).toLocaleTimeString()} ${getTimezoneAbbr()}`;
|
|
64
|
-
} catch { /* silently degrade */ }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function startAutoRefresh() {
|
|
68
|
-
stopAutoRefresh();
|
|
69
|
-
if (state.autoRefresh) {
|
|
70
|
-
state._refreshTimer = setInterval(() => loadAll(), state.autoRefreshInterval * 1000);
|
|
71
|
-
state._quotaTimer = setInterval(() => loadQuota(), state.quotaRefreshInterval * 1000);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function stopAutoRefresh() {
|
|
76
|
-
if (state._refreshTimer) {
|
|
77
|
-
clearInterval(state._refreshTimer);
|
|
78
|
-
state._refreshTimer = null;
|
|
79
|
-
}
|
|
80
|
-
if (state._quotaTimer) {
|
|
81
|
-
clearInterval(state._quotaTimer);
|
|
82
|
-
state._quotaTimer = null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function loadAll() {
|
|
87
|
-
const params = { ...state.dateRange };
|
|
88
|
-
const planParams = { ...state.dateRange, plan: state.plan.plan };
|
|
89
|
-
if (state.plan.customPrice) planParams.customPrice = state.plan.customPrice;
|
|
90
|
-
|
|
91
|
-
const [usage, models, projects, sessions, cost, cache] = await Promise.all([
|
|
92
|
-
fetchUsage({ ...params, granularity: state.granularity }),
|
|
93
|
-
fetchModels(params),
|
|
94
|
-
fetchProjects(params),
|
|
95
|
-
fetchSessions({
|
|
96
|
-
...params,
|
|
97
|
-
project: state.sessionProject,
|
|
98
|
-
sort: state.sessionSort,
|
|
99
|
-
order: state.sessionOrder,
|
|
100
|
-
page: state.sessionPage,
|
|
101
|
-
}),
|
|
102
|
-
fetchCost(planParams),
|
|
103
|
-
fetchCache(params),
|
|
104
|
-
]);
|
|
105
|
-
|
|
106
|
-
// Summary cards
|
|
107
|
-
const t = usage.total;
|
|
108
|
-
const totalAll = t.input_tokens + t.output_tokens + t.cache_read_tokens + t.cache_creation_tokens;
|
|
109
|
-
document.getElementById('val-total-tokens').textContent = formatNumber(totalAll);
|
|
110
|
-
document.getElementById('sub-total-tokens').innerHTML =
|
|
111
|
-
`<span style="color:#4ade80">cache read:${formatNumber(t.cache_read_tokens)}</span> · ` +
|
|
112
|
-
`<span style="color:#f59e0b">cache write:${formatNumber(t.cache_creation_tokens)}</span> · ` +
|
|
113
|
-
`<span style="color:#60a5fa">in:${formatNumber(t.input_tokens)}</span> · ` +
|
|
114
|
-
`<span style="color:#f97316">out:${formatNumber(t.output_tokens)}</span>`;
|
|
115
|
-
document.getElementById('val-api-cost').textContent = `$${cost.api_equivalent_cost_usd.toFixed(2)}`;
|
|
116
|
-
|
|
117
|
-
document.getElementById('val-cache-rate').textContent = `${(cache.cache_read_rate * 100).toFixed(1)}%`;
|
|
118
|
-
|
|
119
|
-
// Set active granularity button
|
|
120
|
-
const activeGran = usage.granularity;
|
|
121
|
-
document.querySelectorAll('#granularity-toggle button').forEach(btn => {
|
|
122
|
-
btn.classList.toggle('active', btn.dataset.granularity === activeGran);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// Charts
|
|
126
|
-
renderTokenTrend(document.getElementById('chart-token-trend'), usage, { yAxis: state.trendYAxis });
|
|
127
|
-
renderCostComparison(document.getElementById('chart-cost-comparison'), cost);
|
|
128
|
-
renderModelDistribution(document.getElementById('chart-model-distribution'), models);
|
|
129
|
-
renderCacheEfficiency(document.getElementById('chart-cache-efficiency'), cache);
|
|
130
|
-
renderProjectDistribution(document.getElementById('chart-project-distribution'), projects);
|
|
131
|
-
renderSessionTable(document.getElementById('session-table'), sessions, {
|
|
132
|
-
onSort: (key) => {
|
|
133
|
-
if (state.sessionSort === key) {
|
|
134
|
-
state.sessionOrder = state.sessionOrder === 'desc' ? 'asc' : 'desc';
|
|
135
|
-
} else {
|
|
136
|
-
state.sessionSort = key;
|
|
137
|
-
state.sessionOrder = 'desc';
|
|
138
|
-
}
|
|
139
|
-
state.sessionPage = 1;
|
|
140
|
-
loadAll();
|
|
141
|
-
},
|
|
142
|
-
onPageChange: (page) => {
|
|
143
|
-
state.sessionPage = page;
|
|
144
|
-
loadAll();
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
updateLastUpdated();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Max bucket limits per granularity to avoid crashing the browser
|
|
152
|
-
const GRANULARITY_MAX_DAYS = { hourly: 14, daily: 90, weekly: 365, monthly: 1825 };
|
|
153
|
-
|
|
154
|
-
function updateGranularityButtons() {
|
|
155
|
-
const { from, to } = state.dateRange;
|
|
156
|
-
const days = (from && to) ? (new Date(to) - new Date(from)) / (1000 * 60 * 60 * 24) : 30;
|
|
157
|
-
document.querySelectorAll('#granularity-toggle button').forEach(btn => {
|
|
158
|
-
const gran = btn.dataset.granularity;
|
|
159
|
-
const maxDays = GRANULARITY_MAX_DAYS[gran] || 9999;
|
|
160
|
-
const tooLarge = days > maxDays;
|
|
161
|
-
btn.disabled = tooLarge;
|
|
162
|
-
btn.title = tooLarge ? `Range too large for ${gran} view (max ${maxDays} days)` : '';
|
|
163
|
-
});
|
|
164
|
-
// If currently selected granularity is now disabled, switch to the finest available
|
|
165
|
-
const currentBtn = document.querySelector(`#granularity-toggle button[data-granularity="${state.granularity}"]`);
|
|
166
|
-
if (currentBtn && currentBtn.disabled) {
|
|
167
|
-
const order = ['hourly', 'daily', 'weekly', 'monthly'];
|
|
168
|
-
const available = order.find(g => {
|
|
169
|
-
const b = document.querySelector(`#granularity-toggle button[data-granularity="${g}"]`);
|
|
170
|
-
return b && !b.disabled;
|
|
171
|
-
});
|
|
172
|
-
if (available) {
|
|
173
|
-
state.granularity = available;
|
|
174
|
-
localStorage.setItem('selectedGranularity', state.granularity);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
// Update active class
|
|
178
|
-
document.querySelectorAll('#granularity-toggle button').forEach(btn => {
|
|
179
|
-
btn.classList.toggle('active', btn.dataset.granularity === state.granularity);
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function init() {
|
|
184
|
-
datePicker = initDatePicker(document.getElementById('date-picker'), (range) => {
|
|
185
|
-
state.dateRange = range;
|
|
186
|
-
state.sessionPage = 1;
|
|
187
|
-
updateGranularityButtons();
|
|
188
|
-
loadAll();
|
|
189
|
-
});
|
|
190
|
-
state.dateRange = datePicker.getRange();
|
|
191
|
-
updateGranularityButtons();
|
|
192
|
-
|
|
193
|
-
planSelector = initPlanSelector(document.getElementById('plan-selector'), (plan) => {
|
|
194
|
-
state.plan = plan;
|
|
195
|
-
loadAll();
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
document.getElementById('granularity-toggle').addEventListener('click', (e) => {
|
|
199
|
-
if (e.target.tagName === 'BUTTON' && !e.target.disabled) {
|
|
200
|
-
state.granularity = e.target.dataset.granularity;
|
|
201
|
-
localStorage.setItem('selectedGranularity', state.granularity);
|
|
202
|
-
loadAll();
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// Y-axis toggle (tokens / dollars)
|
|
207
|
-
const yaxisToggle = document.getElementById('yaxis-toggle');
|
|
208
|
-
yaxisToggle.querySelectorAll('button').forEach(btn => {
|
|
209
|
-
btn.classList.toggle('active', btn.dataset.yaxis === state.trendYAxis);
|
|
210
|
-
});
|
|
211
|
-
yaxisToggle.addEventListener('click', (e) => {
|
|
212
|
-
if (e.target.tagName === 'BUTTON') {
|
|
213
|
-
state.trendYAxis = e.target.dataset.yaxis;
|
|
214
|
-
localStorage.setItem('trendYAxis', state.trendYAxis);
|
|
215
|
-
yaxisToggle.querySelectorAll('button').forEach(btn => {
|
|
216
|
-
btn.classList.toggle('active', btn.dataset.yaxis === state.trendYAxis);
|
|
217
|
-
});
|
|
218
|
-
loadAll();
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
const filterInput = document.getElementById('session-filter');
|
|
223
|
-
let filterTimeout;
|
|
224
|
-
filterInput.addEventListener('input', () => {
|
|
225
|
-
clearTimeout(filterTimeout);
|
|
226
|
-
filterTimeout = setTimeout(() => {
|
|
227
|
-
state.sessionProject = filterInput.value.trim();
|
|
228
|
-
state.sessionPage = 1;
|
|
229
|
-
loadAll();
|
|
230
|
-
}, 300);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
document.getElementById('session-sort').addEventListener('change', (e) => {
|
|
234
|
-
state.sessionSort = e.target.value;
|
|
235
|
-
state.sessionOrder = 'desc';
|
|
236
|
-
state.sessionPage = 1;
|
|
237
|
-
loadAll();
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
document.getElementById('btn-refresh').addEventListener('click', () => { loadAll(); loadQuota(); });
|
|
241
|
-
|
|
242
|
-
const autoToggle = document.getElementById('auto-refresh-toggle');
|
|
243
|
-
autoToggle.checked = state.autoRefresh;
|
|
244
|
-
autoToggle.addEventListener('change', () => {
|
|
245
|
-
state.autoRefresh = autoToggle.checked;
|
|
246
|
-
localStorage.setItem('autoRefresh', state.autoRefresh);
|
|
247
|
-
if (state.autoRefresh) {
|
|
248
|
-
startAutoRefresh();
|
|
249
|
-
} else {
|
|
250
|
-
stopAutoRefresh();
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
// Auto-detect subscription tier
|
|
255
|
-
fetchSubscription().then(info => {
|
|
256
|
-
if (info.plan) {
|
|
257
|
-
planSelector.setDetectedPlan(info.plan);
|
|
258
|
-
state.plan = planSelector.getPlan();
|
|
259
|
-
}
|
|
260
|
-
const tierLabels = { pro: 'Pro', max5x: 'Max 5x', max20x: 'Max 20x' };
|
|
261
|
-
const label = tierLabels[info.plan];
|
|
262
|
-
if (label) {
|
|
263
|
-
const h2 = document.querySelector('#quota-section h2');
|
|
264
|
-
if (h2) h2.textContent = `Subscription Quota (${label})`;
|
|
265
|
-
}
|
|
266
|
-
}).catch(() => {});
|
|
267
|
-
|
|
268
|
-
loadAll();
|
|
269
|
-
loadQuota();
|
|
270
|
-
startAutoRefresh();
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
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
|
+
async function loadQuota() {
|
|
51
|
+
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);
|
|
56
|
+
|
|
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 });
|
|
62
|
+
const el = document.getElementById('quota-last-updated');
|
|
63
|
+
if (el && data.lastFetched) el.textContent = `Updated ${new Date(data.lastFetched).toLocaleTimeString()} ${getTimezoneAbbr()}`;
|
|
64
|
+
} catch { /* silently degrade */ }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function startAutoRefresh() {
|
|
68
|
+
stopAutoRefresh();
|
|
69
|
+
if (state.autoRefresh) {
|
|
70
|
+
state._refreshTimer = setInterval(() => loadAll(), state.autoRefreshInterval * 1000);
|
|
71
|
+
state._quotaTimer = setInterval(() => loadQuota(), state.quotaRefreshInterval * 1000);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function stopAutoRefresh() {
|
|
76
|
+
if (state._refreshTimer) {
|
|
77
|
+
clearInterval(state._refreshTimer);
|
|
78
|
+
state._refreshTimer = null;
|
|
79
|
+
}
|
|
80
|
+
if (state._quotaTimer) {
|
|
81
|
+
clearInterval(state._quotaTimer);
|
|
82
|
+
state._quotaTimer = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function loadAll() {
|
|
87
|
+
const params = { ...state.dateRange };
|
|
88
|
+
const planParams = { ...state.dateRange, plan: state.plan.plan };
|
|
89
|
+
if (state.plan.customPrice) planParams.customPrice = state.plan.customPrice;
|
|
90
|
+
|
|
91
|
+
const [usage, models, projects, sessions, cost, cache] = await Promise.all([
|
|
92
|
+
fetchUsage({ ...params, granularity: state.granularity }),
|
|
93
|
+
fetchModels(params),
|
|
94
|
+
fetchProjects(params),
|
|
95
|
+
fetchSessions({
|
|
96
|
+
...params,
|
|
97
|
+
project: state.sessionProject,
|
|
98
|
+
sort: state.sessionSort,
|
|
99
|
+
order: state.sessionOrder,
|
|
100
|
+
page: state.sessionPage,
|
|
101
|
+
}),
|
|
102
|
+
fetchCost(planParams),
|
|
103
|
+
fetchCache(params),
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
// Summary cards
|
|
107
|
+
const t = usage.total;
|
|
108
|
+
const totalAll = t.input_tokens + t.output_tokens + t.cache_read_tokens + t.cache_creation_tokens;
|
|
109
|
+
document.getElementById('val-total-tokens').textContent = formatNumber(totalAll);
|
|
110
|
+
document.getElementById('sub-total-tokens').innerHTML =
|
|
111
|
+
`<span style="color:#4ade80">cache read:${formatNumber(t.cache_read_tokens)}</span> · ` +
|
|
112
|
+
`<span style="color:#f59e0b">cache write:${formatNumber(t.cache_creation_tokens)}</span> · ` +
|
|
113
|
+
`<span style="color:#60a5fa">in:${formatNumber(t.input_tokens)}</span> · ` +
|
|
114
|
+
`<span style="color:#f97316">out:${formatNumber(t.output_tokens)}</span>`;
|
|
115
|
+
document.getElementById('val-api-cost').textContent = `$${cost.api_equivalent_cost_usd.toFixed(2)}`;
|
|
116
|
+
|
|
117
|
+
document.getElementById('val-cache-rate').textContent = `${(cache.cache_read_rate * 100).toFixed(1)}%`;
|
|
118
|
+
|
|
119
|
+
// Set active granularity button
|
|
120
|
+
const activeGran = usage.granularity;
|
|
121
|
+
document.querySelectorAll('#granularity-toggle button').forEach(btn => {
|
|
122
|
+
btn.classList.toggle('active', btn.dataset.granularity === activeGran);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Charts
|
|
126
|
+
renderTokenTrend(document.getElementById('chart-token-trend'), usage, { yAxis: state.trendYAxis });
|
|
127
|
+
renderCostComparison(document.getElementById('chart-cost-comparison'), cost);
|
|
128
|
+
renderModelDistribution(document.getElementById('chart-model-distribution'), models);
|
|
129
|
+
renderCacheEfficiency(document.getElementById('chart-cache-efficiency'), cache);
|
|
130
|
+
renderProjectDistribution(document.getElementById('chart-project-distribution'), projects);
|
|
131
|
+
renderSessionTable(document.getElementById('session-table'), sessions, {
|
|
132
|
+
onSort: (key) => {
|
|
133
|
+
if (state.sessionSort === key) {
|
|
134
|
+
state.sessionOrder = state.sessionOrder === 'desc' ? 'asc' : 'desc';
|
|
135
|
+
} else {
|
|
136
|
+
state.sessionSort = key;
|
|
137
|
+
state.sessionOrder = 'desc';
|
|
138
|
+
}
|
|
139
|
+
state.sessionPage = 1;
|
|
140
|
+
loadAll();
|
|
141
|
+
},
|
|
142
|
+
onPageChange: (page) => {
|
|
143
|
+
state.sessionPage = page;
|
|
144
|
+
loadAll();
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
updateLastUpdated();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Max bucket limits per granularity to avoid crashing the browser
|
|
152
|
+
const GRANULARITY_MAX_DAYS = { hourly: 14, daily: 90, weekly: 365, monthly: 1825 };
|
|
153
|
+
|
|
154
|
+
function updateGranularityButtons() {
|
|
155
|
+
const { from, to } = state.dateRange;
|
|
156
|
+
const days = (from && to) ? (new Date(to) - new Date(from)) / (1000 * 60 * 60 * 24) : 30;
|
|
157
|
+
document.querySelectorAll('#granularity-toggle button').forEach(btn => {
|
|
158
|
+
const gran = btn.dataset.granularity;
|
|
159
|
+
const maxDays = GRANULARITY_MAX_DAYS[gran] || 9999;
|
|
160
|
+
const tooLarge = days > maxDays;
|
|
161
|
+
btn.disabled = tooLarge;
|
|
162
|
+
btn.title = tooLarge ? `Range too large for ${gran} view (max ${maxDays} days)` : '';
|
|
163
|
+
});
|
|
164
|
+
// If currently selected granularity is now disabled, switch to the finest available
|
|
165
|
+
const currentBtn = document.querySelector(`#granularity-toggle button[data-granularity="${state.granularity}"]`);
|
|
166
|
+
if (currentBtn && currentBtn.disabled) {
|
|
167
|
+
const order = ['hourly', 'daily', 'weekly', 'monthly'];
|
|
168
|
+
const available = order.find(g => {
|
|
169
|
+
const b = document.querySelector(`#granularity-toggle button[data-granularity="${g}"]`);
|
|
170
|
+
return b && !b.disabled;
|
|
171
|
+
});
|
|
172
|
+
if (available) {
|
|
173
|
+
state.granularity = available;
|
|
174
|
+
localStorage.setItem('selectedGranularity', state.granularity);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Update active class
|
|
178
|
+
document.querySelectorAll('#granularity-toggle button').forEach(btn => {
|
|
179
|
+
btn.classList.toggle('active', btn.dataset.granularity === state.granularity);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function init() {
|
|
184
|
+
datePicker = initDatePicker(document.getElementById('date-picker'), (range) => {
|
|
185
|
+
state.dateRange = range;
|
|
186
|
+
state.sessionPage = 1;
|
|
187
|
+
updateGranularityButtons();
|
|
188
|
+
loadAll();
|
|
189
|
+
});
|
|
190
|
+
state.dateRange = datePicker.getRange();
|
|
191
|
+
updateGranularityButtons();
|
|
192
|
+
|
|
193
|
+
planSelector = initPlanSelector(document.getElementById('plan-selector'), (plan) => {
|
|
194
|
+
state.plan = plan;
|
|
195
|
+
loadAll();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
document.getElementById('granularity-toggle').addEventListener('click', (e) => {
|
|
199
|
+
if (e.target.tagName === 'BUTTON' && !e.target.disabled) {
|
|
200
|
+
state.granularity = e.target.dataset.granularity;
|
|
201
|
+
localStorage.setItem('selectedGranularity', state.granularity);
|
|
202
|
+
loadAll();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Y-axis toggle (tokens / dollars)
|
|
207
|
+
const yaxisToggle = document.getElementById('yaxis-toggle');
|
|
208
|
+
yaxisToggle.querySelectorAll('button').forEach(btn => {
|
|
209
|
+
btn.classList.toggle('active', btn.dataset.yaxis === state.trendYAxis);
|
|
210
|
+
});
|
|
211
|
+
yaxisToggle.addEventListener('click', (e) => {
|
|
212
|
+
if (e.target.tagName === 'BUTTON') {
|
|
213
|
+
state.trendYAxis = e.target.dataset.yaxis;
|
|
214
|
+
localStorage.setItem('trendYAxis', state.trendYAxis);
|
|
215
|
+
yaxisToggle.querySelectorAll('button').forEach(btn => {
|
|
216
|
+
btn.classList.toggle('active', btn.dataset.yaxis === state.trendYAxis);
|
|
217
|
+
});
|
|
218
|
+
loadAll();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const filterInput = document.getElementById('session-filter');
|
|
223
|
+
let filterTimeout;
|
|
224
|
+
filterInput.addEventListener('input', () => {
|
|
225
|
+
clearTimeout(filterTimeout);
|
|
226
|
+
filterTimeout = setTimeout(() => {
|
|
227
|
+
state.sessionProject = filterInput.value.trim();
|
|
228
|
+
state.sessionPage = 1;
|
|
229
|
+
loadAll();
|
|
230
|
+
}, 300);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
document.getElementById('session-sort').addEventListener('change', (e) => {
|
|
234
|
+
state.sessionSort = e.target.value;
|
|
235
|
+
state.sessionOrder = 'desc';
|
|
236
|
+
state.sessionPage = 1;
|
|
237
|
+
loadAll();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
document.getElementById('btn-refresh').addEventListener('click', () => { loadAll(); loadQuota(); });
|
|
241
|
+
|
|
242
|
+
const autoToggle = document.getElementById('auto-refresh-toggle');
|
|
243
|
+
autoToggle.checked = state.autoRefresh;
|
|
244
|
+
autoToggle.addEventListener('change', () => {
|
|
245
|
+
state.autoRefresh = autoToggle.checked;
|
|
246
|
+
localStorage.setItem('autoRefresh', state.autoRefresh);
|
|
247
|
+
if (state.autoRefresh) {
|
|
248
|
+
startAutoRefresh();
|
|
249
|
+
} else {
|
|
250
|
+
stopAutoRefresh();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Auto-detect subscription tier
|
|
255
|
+
fetchSubscription().then(info => {
|
|
256
|
+
if (info.plan) {
|
|
257
|
+
planSelector.setDetectedPlan(info.plan);
|
|
258
|
+
state.plan = planSelector.getPlan();
|
|
259
|
+
}
|
|
260
|
+
const tierLabels = { pro: 'Pro', max5x: 'Max 5x', max20x: 'Max 20x' };
|
|
261
|
+
const label = tierLabels[info.plan];
|
|
262
|
+
if (label) {
|
|
263
|
+
const h2 = document.querySelector('#quota-section h2');
|
|
264
|
+
if (h2) h2.textContent = `Subscription Quota (${label})`;
|
|
265
|
+
}
|
|
266
|
+
}).catch(() => {});
|
|
267
|
+
|
|
268
|
+
loadAll();
|
|
269
|
+
loadQuota();
|
|
270
|
+
startAutoRefresh();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
export function renderCacheEfficiency(container, data) {
|
|
2
|
-
container.innerHTML = '';
|
|
3
|
-
|
|
4
|
-
const items = [
|
|
5
|
-
{ label: 'Cache Read', value: data.cache_read_rate, color: '#4ade80', tokens: data.cache_read_tokens },
|
|
6
|
-
{ label: 'Cache Creation', value: data.cache_creation_rate, color: '#f59e0b', tokens: data.cache_creation_tokens },
|
|
7
|
-
{ label: 'No Cache', value: data.no_cache_rate, color: '#ef4444', tokens: data.non_cached_input_tokens },
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
for (const item of items) {
|
|
11
|
-
const row = document.createElement('div');
|
|
12
|
-
row.style.marginBottom = '12px';
|
|
13
|
-
|
|
14
|
-
const header = document.createElement('div');
|
|
15
|
-
header.style.cssText = 'display:flex;justify-content:space-between;font-size:11px;color:#94a3b8;margin-bottom:4px';
|
|
16
|
-
header.innerHTML = `<span>${item.label}</span><span>${(item.value * 100).toFixed(1)}%</span>`;
|
|
17
|
-
|
|
18
|
-
const barBg = document.createElement('div');
|
|
19
|
-
barBg.style.cssText = 'height:8px;background:#334155;border-radius:4px;overflow:hidden';
|
|
20
|
-
|
|
21
|
-
const barFill = document.createElement('div');
|
|
22
|
-
barFill.style.cssText = `width:${item.value * 100}%;height:100%;background:${item.color};border-radius:4px;transition:width 0.5s`;
|
|
23
|
-
|
|
24
|
-
barBg.appendChild(barFill);
|
|
25
|
-
row.appendChild(header);
|
|
26
|
-
row.appendChild(barBg);
|
|
27
|
-
container.appendChild(row);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
1
|
+
export function renderCacheEfficiency(container, data) {
|
|
2
|
+
container.innerHTML = '';
|
|
3
|
+
|
|
4
|
+
const items = [
|
|
5
|
+
{ label: 'Cache Read', value: data.cache_read_rate, color: '#4ade80', tokens: data.cache_read_tokens },
|
|
6
|
+
{ label: 'Cache Creation', value: data.cache_creation_rate, color: '#f59e0b', tokens: data.cache_creation_tokens },
|
|
7
|
+
{ label: 'No Cache', value: data.no_cache_rate, color: '#ef4444', tokens: data.non_cached_input_tokens },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
for (const item of items) {
|
|
11
|
+
const row = document.createElement('div');
|
|
12
|
+
row.style.marginBottom = '12px';
|
|
13
|
+
|
|
14
|
+
const header = document.createElement('div');
|
|
15
|
+
header.style.cssText = 'display:flex;justify-content:space-between;font-size:11px;color:#94a3b8;margin-bottom:4px';
|
|
16
|
+
header.innerHTML = `<span>${item.label}</span><span>${(item.value * 100).toFixed(1)}%</span>`;
|
|
17
|
+
|
|
18
|
+
const barBg = document.createElement('div');
|
|
19
|
+
barBg.style.cssText = 'height:8px;background:#334155;border-radius:4px;overflow:hidden';
|
|
20
|
+
|
|
21
|
+
const barFill = document.createElement('div');
|
|
22
|
+
barFill.style.cssText = `width:${item.value * 100}%;height:100%;background:${item.color};border-radius:4px;transition:width 0.5s`;
|
|
23
|
+
|
|
24
|
+
barBg.appendChild(barFill);
|
|
25
|
+
row.appendChild(header);
|
|
26
|
+
row.appendChild(barBg);
|
|
27
|
+
container.appendChild(row);
|
|
28
|
+
}
|
|
29
|
+
}
|