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/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);