claude-usage-dashboard 1.5.1 → 1.5.2

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.
@@ -1,209 +1,209 @@
1
- const fmt = (n) => {
2
- if (n == null) return '—';
3
- if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
4
- if (n >= 1_000) return (n / 1_000).toFixed(0) + 'K';
5
- return n.toString();
6
- };
7
-
8
- const fmtCost = (n) => n == null ? '—' : `$${n.toFixed(2)}`;
9
-
10
- const fmtDate = (iso) => {
11
- const d = new Date(iso);
12
- return `${d.getMonth() + 1}/${d.getDate()}`;
13
- };
14
-
15
- function getModelData(cycle, modelKey) {
16
- if (modelKey === 'overall') return cycle.overall;
17
- return cycle.models?.[modelKey] || { utilization: 0, actualTokens: 0, projectedTokensAt100: null, actualCost: 0, projectedCostAt100: null, tokens: { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 } };
18
- }
19
-
20
- const MAX_DISPLAY_CYCLES = 10;
21
-
22
- export function renderQuotaCycles(container, data, { modelKey = 'overall' } = {}) {
23
- if (!container) return;
24
-
25
- const hasModelData = data.currentCycle &&
26
- ((data.currentCycle.models?.opus?.utilization > 0) ||
27
- (data.currentCycle.models?.sonnet?.utilization > 0));
28
-
29
- // --- Model toggle: grey out when no data ---
30
- const toggleEl = document.getElementById('cycle-model-toggle');
31
- if (toggleEl) {
32
- toggleEl.querySelectorAll('button[data-cycle-model="opus"], button[data-cycle-model="sonnet"]').forEach(btn => {
33
- btn.disabled = !hasModelData;
34
- });
35
- }
36
-
37
- // --- Inline projection summary in header ---
38
- const summaryEl = document.getElementById('cycle-projection-summary');
39
- if (summaryEl) {
40
- summaryEl.textContent = '';
41
- }
42
-
43
- // --- Bar Chart (compact) ---
44
- container.innerHTML = '';
45
-
46
- const allCycles = [];
47
- if (data.history) allCycles.push(...[...data.history].reverse());
48
- if (data.currentCycle) allCycles.push(data.currentCycle);
49
-
50
- if (allCycles.length === 0) {
51
- container.innerHTML = '<div style="color:#64748b;text-align:center;padding:20px;font-size:12px">No cycle data yet.</div>';
52
- return;
53
- }
54
-
55
- const displayAll = allCycles.slice(-MAX_DISPLAY_CYCLES);
56
-
57
- const chartData = displayAll.map(c => {
58
- const d = getModelData(c, modelKey);
59
- return {
60
- label: `${fmtDate(c.start)}–${fmtDate(c.resets_at)}`,
61
- actual: d.actualTokens,
62
- projected: d.projectedTokensAt100,
63
- isCurrent: c === data.currentCycle,
64
- };
65
- });
66
-
67
- // Horizontal bar chart
68
- const rowHeight = 28;
69
- const margin = { top: 20, right: 50, bottom: 6, left: 70 };
70
- const width = container.clientWidth - margin.left - margin.right;
71
- const height = chartData.length * rowHeight;
72
-
73
- const svg = d3.select(container).append('svg')
74
- .attr('width', width + margin.left + margin.right)
75
- .attr('height', height + margin.top + margin.bottom)
76
- .append('g').attr('transform', `translate(${margin.left},${margin.top})`);
77
-
78
- const y0 = d3.scaleBand().domain(chartData.map(d => d.label)).range([0, height]).padding(0.25);
79
- const maxVal = d3.max(chartData, d => Math.max(d.actual, d.projected || 0)) || 1;
80
- const x = d3.scaleLinear().domain([0, maxVal * 1.1]).range([0, width]);
81
-
82
- // Axes
83
- svg.append('g')
84
- .call(d3.axisLeft(y0).tickSize(0))
85
- .selectAll('text').attr('fill', '#94a3b8').style('font-size', '9px');
86
- svg.append('g').attr('transform', `translate(0,${height})`)
87
- .call(d3.axisBottom(x).ticks(4).tickFormat(d => fmt(d)))
88
- .selectAll('text').attr('fill', '#94a3b8').style('font-size', '9px');
89
- svg.selectAll('.domain, .tick line').attr('stroke', '#334155');
90
-
91
- const barH = Math.min(y0.bandwidth() / 2.5, 12);
92
-
93
- // Projected bars (behind, semi-transparent)
94
- svg.selectAll('.bar-projected').data(chartData.filter(d => d.projected != null))
95
- .join('rect').attr('class', 'bar-projected')
96
- .attr('x', 0)
97
- .attr('width', d => x(d.projected))
98
- .attr('y', d => y0(d.label) + y0.bandwidth() / 2 - barH)
99
- .attr('height', barH * 2)
100
- .attr('fill', '#f59e0b').attr('opacity', 0.2)
101
- .attr('rx', 2);
102
-
103
- // Tooltip
104
- const tooltip = d3.select(container).append('div')
105
- .attr('class', 'd3-tooltip')
106
- .style('opacity', 0);
107
-
108
- function showTip(event, d) {
109
- const proj = d.projected != null ? fmt(d.projected) : '—';
110
- const rect = container.getBoundingClientRect();
111
- tooltip.html(`<strong>${d.label}</strong><br>Actual: ${fmt(d.actual)}<br>Proj@100%: ${proj}`)
112
- .style('opacity', 1)
113
- .style('left', (event.clientX - rect.left + 12) + 'px')
114
- .style('top', (event.clientY - rect.top - 10) + 'px');
115
- }
116
- function hideTip() { tooltip.style('opacity', 0); }
117
-
118
- // Actual bars (front)
119
- svg.selectAll('.bar-actual').data(chartData)
120
- .join('rect').attr('class', 'bar-actual')
121
- .attr('x', 0)
122
- .attr('width', d => x(d.actual))
123
- .attr('y', d => y0(d.label) + y0.bandwidth() / 2 - barH / 2)
124
- .attr('height', barH)
125
- .attr('fill', d => d.isCurrent ? '#3b82f6' : '#60a5fa')
126
- .attr('rx', 2);
127
-
128
- // Hover areas (full row for easy targeting)
129
- svg.selectAll('.bar-hover').data(chartData)
130
- .join('rect').attr('class', 'bar-hover')
131
- .attr('x', 0)
132
- .attr('width', width)
133
- .attr('y', d => y0(d.label))
134
- .attr('height', y0.bandwidth())
135
- .attr('fill', 'transparent')
136
- .style('cursor', 'pointer')
137
- .on('mousemove', showTip)
138
- .on('mouseleave', hideTip);
139
-
140
- // Compact legend
141
- const legend = svg.append('g').attr('transform', `translate(0, -8)`);
142
- legend.append('rect').attr('width', 8).attr('height', 8).attr('fill', '#60a5fa').attr('rx', 1);
143
- legend.append('text').attr('x', 10).attr('y', 7).text('Actual').attr('fill', '#64748b').style('font-size', '9px');
144
- legend.append('rect').attr('x', 50).attr('width', 8).attr('height', 8).attr('fill', '#f59e0b').attr('opacity', 0.4).attr('rx', 1);
145
- legend.append('text').attr('x', 60).attr('y', 7).text('Proj@100%').attr('fill', '#64748b').style('font-size', '9px');
146
-
147
- // --- History Table ---
148
- const tableEl = document.getElementById('quota-cycles-table');
149
- if (!tableEl) return;
150
- tableEl.innerHTML = '';
151
-
152
- const table = document.createElement('table');
153
- const thead = document.createElement('thead');
154
- thead.innerHTML = `<tr>
155
- <th>Cycle</th>
156
- <th class="align-right">Util%</th>
157
- <th class="align-right">In</th>
158
- <th class="align-right">Out</th>
159
- <th class="align-right">CR</th>
160
- <th class="align-right">CW</th>
161
- <th class="align-right">Total</th>
162
- <th class="align-right">Excl CR</th>
163
- <th class="align-right">Cost</th>
164
- <th class="align-right col-highlight">Proj Tokens</th>
165
- <th class="align-right col-highlight">Proj Cost</th>
166
- <th class="align-right">\u0394 Prev</th>
167
- </tr>`;
168
- table.appendChild(thead);
169
-
170
- const tbody = document.createElement('tbody');
171
- const displayCycles = [...displayAll].reverse();
172
- for (let i = 0; i < displayCycles.length; i++) {
173
- const c = displayCycles[i];
174
- const d = getModelData(c, modelKey);
175
- const t = d.tokens || { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
176
- const totalInclCR = t.input + t.output + t.cacheRead + t.cacheCreation;
177
- const prev = displayCycles[i + 1] ? getModelData(displayCycles[i + 1], modelKey) : null;
178
-
179
- let deltaStr = '—';
180
- let deltaClass = '';
181
- if (prev && prev.projectedTokensAt100 != null && d.projectedTokensAt100 != null && prev.projectedTokensAt100 > 0) {
182
- const delta = ((d.projectedTokensAt100 - prev.projectedTokensAt100) / prev.projectedTokensAt100) * 100;
183
- deltaStr = `${delta >= 0 ? '+' : ''}${delta.toFixed(1)}%`;
184
- deltaClass = delta >= 0 ? 'delta-positive' : 'delta-negative';
185
- }
186
-
187
- const isCurrent = c === data.currentCycle;
188
- const label = `${fmtDate(c.start)}–${fmtDate(c.resets_at)}${isCurrent ? ' *' : ''}`;
189
-
190
- const tr = document.createElement('tr');
191
- tr.innerHTML = `
192
- <td>${label}</td>
193
- <td class="align-right">${d.utilization.toFixed(1)}%</td>
194
- <td class="align-right">${fmt(t.input)}</td>
195
- <td class="align-right">${fmt(t.output)}</td>
196
- <td class="align-right">${fmt(t.cacheRead)}</td>
197
- <td class="align-right">${fmt(t.cacheCreation)}</td>
198
- <td class="align-right">${fmt(totalInclCR)}</td>
199
- <td class="align-right">${fmt(d.actualTokens)}</td>
200
- <td class="align-right">${fmtCost(d.actualCost)}</td>
201
- <td class="align-right col-highlight">${fmt(d.projectedTokensAt100)}</td>
202
- <td class="align-right col-highlight">${fmtCost(d.projectedCostAt100)}</td>
203
- <td class="align-right ${deltaClass}">${deltaStr}</td>
204
- `;
205
- tbody.appendChild(tr);
206
- }
207
- table.appendChild(tbody);
208
- tableEl.appendChild(table);
209
- }
1
+ const fmt = (n) => {
2
+ if (n == null) return '—';
3
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
4
+ if (n >= 1_000) return (n / 1_000).toFixed(0) + 'K';
5
+ return n.toString();
6
+ };
7
+
8
+ const fmtCost = (n) => n == null ? '—' : `$${n.toFixed(2)}`;
9
+
10
+ const fmtDate = (iso) => {
11
+ const d = new Date(iso);
12
+ return `${d.getMonth() + 1}/${d.getDate()}`;
13
+ };
14
+
15
+ function getModelData(cycle, modelKey) {
16
+ if (modelKey === 'overall') return cycle.overall;
17
+ return cycle.models?.[modelKey] || { utilization: 0, actualTokens: 0, projectedTokensAt100: null, actualCost: 0, projectedCostAt100: null, tokens: { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 } };
18
+ }
19
+
20
+ const MAX_DISPLAY_CYCLES = 10;
21
+
22
+ export function renderQuotaCycles(container, data, { modelKey = 'overall' } = {}) {
23
+ if (!container) return;
24
+
25
+ const hasModelData = data.currentCycle &&
26
+ ((data.currentCycle.models?.opus?.utilization > 0) ||
27
+ (data.currentCycle.models?.sonnet?.utilization > 0));
28
+
29
+ // --- Model toggle: grey out when no data ---
30
+ const toggleEl = document.getElementById('cycle-model-toggle');
31
+ if (toggleEl) {
32
+ toggleEl.querySelectorAll('button[data-cycle-model="opus"], button[data-cycle-model="sonnet"]').forEach(btn => {
33
+ btn.disabled = !hasModelData;
34
+ });
35
+ }
36
+
37
+ // --- Inline projection summary in header ---
38
+ const summaryEl = document.getElementById('cycle-projection-summary');
39
+ if (summaryEl) {
40
+ summaryEl.textContent = '';
41
+ }
42
+
43
+ // --- Bar Chart (compact) ---
44
+ container.innerHTML = '';
45
+
46
+ const allCycles = [];
47
+ if (data.history) allCycles.push(...[...data.history].reverse());
48
+ if (data.currentCycle) allCycles.push(data.currentCycle);
49
+
50
+ if (allCycles.length === 0) {
51
+ container.innerHTML = '<div style="color:#64748b;text-align:center;padding:20px;font-size:12px">No cycle data yet.</div>';
52
+ return;
53
+ }
54
+
55
+ const displayAll = allCycles.slice(-MAX_DISPLAY_CYCLES);
56
+
57
+ const chartData = displayAll.map(c => {
58
+ const d = getModelData(c, modelKey);
59
+ return {
60
+ label: `${fmtDate(c.start)}–${fmtDate(c.resets_at)}`,
61
+ actual: d.actualTokens,
62
+ projected: d.projectedTokensAt100,
63
+ isCurrent: c === data.currentCycle,
64
+ };
65
+ });
66
+
67
+ // Horizontal bar chart
68
+ const rowHeight = 28;
69
+ const margin = { top: 20, right: 50, bottom: 6, left: 70 };
70
+ const width = container.clientWidth - margin.left - margin.right;
71
+ const height = chartData.length * rowHeight;
72
+
73
+ const svg = d3.select(container).append('svg')
74
+ .attr('width', width + margin.left + margin.right)
75
+ .attr('height', height + margin.top + margin.bottom)
76
+ .append('g').attr('transform', `translate(${margin.left},${margin.top})`);
77
+
78
+ const y0 = d3.scaleBand().domain(chartData.map(d => d.label)).range([0, height]).padding(0.25);
79
+ const maxVal = d3.max(chartData, d => Math.max(d.actual, d.projected || 0)) || 1;
80
+ const x = d3.scaleLinear().domain([0, maxVal * 1.1]).range([0, width]);
81
+
82
+ // Axes
83
+ svg.append('g')
84
+ .call(d3.axisLeft(y0).tickSize(0))
85
+ .selectAll('text').attr('fill', '#94a3b8').style('font-size', '9px');
86
+ svg.append('g').attr('transform', `translate(0,${height})`)
87
+ .call(d3.axisBottom(x).ticks(4).tickFormat(d => fmt(d)))
88
+ .selectAll('text').attr('fill', '#94a3b8').style('font-size', '9px');
89
+ svg.selectAll('.domain, .tick line').attr('stroke', '#334155');
90
+
91
+ const barH = Math.min(y0.bandwidth() / 2.5, 12);
92
+
93
+ // Projected bars (behind, semi-transparent)
94
+ svg.selectAll('.bar-projected').data(chartData.filter(d => d.projected != null))
95
+ .join('rect').attr('class', 'bar-projected')
96
+ .attr('x', 0)
97
+ .attr('width', d => x(d.projected))
98
+ .attr('y', d => y0(d.label) + y0.bandwidth() / 2 - barH)
99
+ .attr('height', barH * 2)
100
+ .attr('fill', '#f59e0b').attr('opacity', 0.2)
101
+ .attr('rx', 2);
102
+
103
+ // Tooltip
104
+ const tooltip = d3.select(container).append('div')
105
+ .attr('class', 'd3-tooltip')
106
+ .style('opacity', 0);
107
+
108
+ function showTip(event, d) {
109
+ const proj = d.projected != null ? fmt(d.projected) : '—';
110
+ const rect = container.getBoundingClientRect();
111
+ tooltip.html(`<strong>${d.label}</strong><br>Actual: ${fmt(d.actual)}<br>Proj@100%: ${proj}`)
112
+ .style('opacity', 1)
113
+ .style('left', (event.clientX - rect.left + 12) + 'px')
114
+ .style('top', (event.clientY - rect.top - 10) + 'px');
115
+ }
116
+ function hideTip() { tooltip.style('opacity', 0); }
117
+
118
+ // Actual bars (front)
119
+ svg.selectAll('.bar-actual').data(chartData)
120
+ .join('rect').attr('class', 'bar-actual')
121
+ .attr('x', 0)
122
+ .attr('width', d => x(d.actual))
123
+ .attr('y', d => y0(d.label) + y0.bandwidth() / 2 - barH / 2)
124
+ .attr('height', barH)
125
+ .attr('fill', d => d.isCurrent ? '#3b82f6' : '#60a5fa')
126
+ .attr('rx', 2);
127
+
128
+ // Hover areas (full row for easy targeting)
129
+ svg.selectAll('.bar-hover').data(chartData)
130
+ .join('rect').attr('class', 'bar-hover')
131
+ .attr('x', 0)
132
+ .attr('width', width)
133
+ .attr('y', d => y0(d.label))
134
+ .attr('height', y0.bandwidth())
135
+ .attr('fill', 'transparent')
136
+ .style('cursor', 'pointer')
137
+ .on('mousemove', showTip)
138
+ .on('mouseleave', hideTip);
139
+
140
+ // Compact legend
141
+ const legend = svg.append('g').attr('transform', `translate(0, -8)`);
142
+ legend.append('rect').attr('width', 8).attr('height', 8).attr('fill', '#60a5fa').attr('rx', 1);
143
+ legend.append('text').attr('x', 10).attr('y', 7).text('Actual').attr('fill', '#64748b').style('font-size', '9px');
144
+ legend.append('rect').attr('x', 50).attr('width', 8).attr('height', 8).attr('fill', '#f59e0b').attr('opacity', 0.4).attr('rx', 1);
145
+ legend.append('text').attr('x', 60).attr('y', 7).text('Proj@100%').attr('fill', '#64748b').style('font-size', '9px');
146
+
147
+ // --- History Table ---
148
+ const tableEl = document.getElementById('quota-cycles-table');
149
+ if (!tableEl) return;
150
+ tableEl.innerHTML = '';
151
+
152
+ const table = document.createElement('table');
153
+ const thead = document.createElement('thead');
154
+ thead.innerHTML = `<tr>
155
+ <th>Cycle</th>
156
+ <th class="align-right">Util%</th>
157
+ <th class="align-right">In</th>
158
+ <th class="align-right">Out</th>
159
+ <th class="align-right">CR</th>
160
+ <th class="align-right">CW</th>
161
+ <th class="align-right">Total</th>
162
+ <th class="align-right">Excl CR</th>
163
+ <th class="align-right">Cost</th>
164
+ <th class="align-right col-highlight">Proj Tokens</th>
165
+ <th class="align-right col-highlight">Proj Cost</th>
166
+ <th class="align-right">\u0394 Prev</th>
167
+ </tr>`;
168
+ table.appendChild(thead);
169
+
170
+ const tbody = document.createElement('tbody');
171
+ const displayCycles = [...displayAll].reverse();
172
+ for (let i = 0; i < displayCycles.length; i++) {
173
+ const c = displayCycles[i];
174
+ const d = getModelData(c, modelKey);
175
+ const t = d.tokens || { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
176
+ const totalInclCR = t.input + t.output + t.cacheRead + t.cacheCreation;
177
+ const prev = displayCycles[i + 1] ? getModelData(displayCycles[i + 1], modelKey) : null;
178
+
179
+ let deltaStr = '—';
180
+ let deltaClass = '';
181
+ if (prev && prev.projectedTokensAt100 != null && d.projectedTokensAt100 != null && prev.projectedTokensAt100 > 0) {
182
+ const delta = ((d.projectedTokensAt100 - prev.projectedTokensAt100) / prev.projectedTokensAt100) * 100;
183
+ deltaStr = `${delta >= 0 ? '+' : ''}${delta.toFixed(1)}%`;
184
+ deltaClass = delta >= 0 ? 'delta-positive' : 'delta-negative';
185
+ }
186
+
187
+ const isCurrent = c === data.currentCycle;
188
+ const label = `${fmtDate(c.start)}–${fmtDate(c.resets_at)}${isCurrent ? ' *' : ''}`;
189
+
190
+ const tr = document.createElement('tr');
191
+ tr.innerHTML = `
192
+ <td>${label}</td>
193
+ <td class="align-right">${d.utilization.toFixed(1)}%</td>
194
+ <td class="align-right">${fmt(t.input)}</td>
195
+ <td class="align-right">${fmt(t.output)}</td>
196
+ <td class="align-right">${fmt(t.cacheRead)}</td>
197
+ <td class="align-right">${fmt(t.cacheCreation)}</td>
198
+ <td class="align-right">${fmt(totalInclCR)}</td>
199
+ <td class="align-right">${fmt(d.actualTokens)}</td>
200
+ <td class="align-right">${fmtCost(d.actualCost)}</td>
201
+ <td class="align-right col-highlight">${fmt(d.projectedTokensAt100)}</td>
202
+ <td class="align-right col-highlight">${fmtCost(d.projectedCostAt100)}</td>
203
+ <td class="align-right ${deltaClass}">${deltaStr}</td>
204
+ `;
205
+ tbody.appendChild(tr);
206
+ }
207
+ table.appendChild(tbody);
208
+ tableEl.appendChild(table);
209
+ }