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/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
+ }