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