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.
- 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 +326 -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 -45
- package/server/parser.js +129 -129
- package/server/pricing.js +52 -52
- package/server/quota-cycles.js +274 -274
- package/server/routes/api.js +175 -175
- package/server/sync.js +69 -69
|
@@ -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
|
+
}
|