@krtclcdy/tersio 2.16.0 → 2.18.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/README.md +18 -2
- package/cli/dashboard.js +4 -0
- package/cli/usage.js +21 -6
- package/dashboard/app.js +395 -89
- package/dashboard/styles.css +39 -5
- package/dashboard/template.html +47 -19
- package/extensions/caveman-session/index.js +3 -4
- package/extensions/combo-toggle/index.js +3 -4
- package/extensions/rtk-session/index.js +3 -4
- package/extensions/shared/rtk-gain.js +16 -1
- package/extensions/shared/session-state.js +15 -0
- package/extensions/shared/usage-ledger.js +72 -9
- package/package.json +1 -1
package/dashboard/app.js
CHANGED
|
@@ -27,9 +27,25 @@
|
|
|
27
27
|
var savedFx = JSON.parse(localStorage.getItem('tersio-fx') || 'null');
|
|
28
28
|
if (savedFx && savedFx.rates && savedFx.rates.USD === 1) fx.rates = savedFx.rates;
|
|
29
29
|
} catch (e) { }
|
|
30
|
+
// Magnitude-aware decimals. Model prices keep falling, so a flat 2dp rounds
|
|
31
|
+
// real spend down to "$0.00"; small amounts keep enough digits to stay
|
|
32
|
+
// readable (0.000187, not 0). Larger amounts use the currency's own scale.
|
|
33
|
+
function moneyDecimals(v, base) {
|
|
34
|
+
var a = Math.abs(v);
|
|
35
|
+
if (a >= 0.1 || a === 0) return base;
|
|
36
|
+
if (a >= 0.001) return Math.max(base, 4);
|
|
37
|
+
if (a >= 0.00001) return Math.max(base, 6);
|
|
38
|
+
return Math.max(base, 8);
|
|
39
|
+
}
|
|
30
40
|
function fxMoney(v) {
|
|
31
|
-
var c = CURS[fx.cur] || CURS.USD, r = fx.rates[fx.cur] || 1;
|
|
32
|
-
|
|
41
|
+
var c = CURS[fx.cur] || CURS.USD, r = fx.rates[fx.cur] || 1, amt = v * r;
|
|
42
|
+
var d = moneyDecimals(amt, c.d);
|
|
43
|
+
return c.s + amt.toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d });
|
|
44
|
+
}
|
|
45
|
+
// Tooltips build innerHTML, and error notes come from providers, so they are
|
|
46
|
+
// never interpolated raw.
|
|
47
|
+
function esc(s) {
|
|
48
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
function countUp(el, target, money) {
|
|
@@ -164,7 +180,9 @@
|
|
|
164
180
|
});
|
|
165
181
|
})();
|
|
166
182
|
|
|
183
|
+
var OPENAI_SVG = '<svg viewBox="0 0 24 24" fill="#000" aria-hidden="true"><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>';
|
|
167
184
|
var PROVIDERS = [
|
|
185
|
+
[/openai|codex|gpt-|o1/i, 'OpenAI', 'openai', '#fff', OPENAI_SVG],
|
|
168
186
|
[/muse/i, 'Meta', 'meta', '#0082fb'],
|
|
169
187
|
[/deepseek/i, 'DeepSeek', 'deepseek', '#4d6bfe'],
|
|
170
188
|
[/qwen|qwq/i, 'Qwen', 'qwen', '#6950EF'],
|
|
@@ -175,21 +193,27 @@
|
|
|
175
193
|
[/nemotron|nvidia/i, 'NVIDIA', 'nvidia', '#76b900'],
|
|
176
194
|
[/mistral/i, 'Mistral', 'mistralai', '#ff7000'],
|
|
177
195
|
[/claude|anthropic/i, 'Anthropic', 'anthropic', '#d97757'],
|
|
178
|
-
[/gemini|google/i, 'Google', '
|
|
196
|
+
[/gemini|google|gemma/i, 'Google', 'google', '#4285F4'],
|
|
179
197
|
];
|
|
180
198
|
function vendorOf(model) {
|
|
181
199
|
for (var i = 0; i < PROVIDERS.length; i++) {
|
|
182
|
-
if (PROVIDERS[i][0].test(model)) return { name: PROVIDERS[i][1], slug: PROVIDERS[i][2], color: PROVIDERS[i][3] };
|
|
200
|
+
if (PROVIDERS[i][0].test(model)) return { name: PROVIDERS[i][1], slug: PROVIDERS[i][2], color: PROVIDERS[i][3], svg: PROVIDERS[i][4] };
|
|
183
201
|
}
|
|
184
202
|
return { name: 'Other', slug: '', color: '#71717a' };
|
|
185
203
|
}
|
|
186
|
-
// Brand glyph
|
|
187
|
-
//
|
|
204
|
+
// Brand glyph: inline SVG when the provider ships one (OpenAI), else the
|
|
205
|
+
// Simple Icons CDN; unknown / Other providers render a clean Lucide
|
|
206
|
+
// sparkles icon with no solid background as a placeholder.
|
|
188
207
|
function brandHTML(v, small) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
208
|
+
if (v.svg) {
|
|
209
|
+
return '<span class="brandmark' + (small ? ' sm' : '') + '" style="background:' + v.color + '" title="' + v.name + '">' + v.svg + '</span>';
|
|
210
|
+
}
|
|
211
|
+
if (!v.slug) {
|
|
212
|
+
return '<span class="brandmark placeholder' + (small ? ' sm' : '') + '" title="' + v.name + '">' +
|
|
213
|
+
'<i data-lucide="sparkles" class="fb" style="display:grid"></i></span>';
|
|
214
|
+
}
|
|
215
|
+
var icon = '<img src="https://cdn.simpleicons.org/' + v.slug + '/white" alt="" loading="lazy" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'grid\';">';
|
|
216
|
+
var fb = '<i data-lucide="sparkles" class="fb"></i>';
|
|
193
217
|
return '<span class="brandmark' + (small ? ' sm' : '') + '" style="background:' + v.color + '" title="' + v.name + '">' + icon + fb + '</span>';
|
|
194
218
|
}
|
|
195
219
|
var PALETTE = ['#34d399', '#818cf8', '#22d3ee', '#fbbf24', '#f472b6', '#a78bfa', '#fb923c', '#2dd4bf'];
|
|
@@ -197,12 +221,30 @@
|
|
|
197
221
|
function dayKey(d) {
|
|
198
222
|
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
199
223
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
224
|
+
// Absolute local stamp for the When column: "Sep 18, 11:14:08 PM". The
|
|
225
|
+
// column is sortable, and a relative label made that ordering unreadable.
|
|
226
|
+
function whenStamp(ts) {
|
|
227
|
+
var d = new Date(ts);
|
|
228
|
+
return d.toLocaleString('en-US', { month: 'short', day: 'numeric' }) + ', ' +
|
|
229
|
+
d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', second: '2-digit' });
|
|
230
|
+
}
|
|
231
|
+
// Per-request generation time + output throughput from the message-level
|
|
232
|
+
// `duration` (ms). Missing → '–'. Throughput uses output tokens since
|
|
233
|
+
// that is what streams to the user during the measured window.
|
|
234
|
+
function fmtDur(ms) {
|
|
235
|
+
if (ms === undefined) return '–';
|
|
236
|
+
var s = ms / 1000;
|
|
237
|
+
return (s < 10 ? s.toFixed(1) : Math.round(s)) + 's';
|
|
238
|
+
}
|
|
239
|
+
function speedText(r) {
|
|
240
|
+
if (r.d === undefined) return '–';
|
|
241
|
+
var tps = r.d > 0 ? r.o / (r.d / 1000) : 0;
|
|
242
|
+
return fmtDur(r.d) + ' ⚡' + (tps < 10 ? tps.toFixed(1) : Math.round(tps)) + '/s';
|
|
243
|
+
}
|
|
244
|
+
function speedTitle(r) {
|
|
245
|
+
if (r.d === undefined) return 'no duration recorded';
|
|
246
|
+
var tps = r.d > 0 ? r.o / (r.d / 1000) : 0;
|
|
247
|
+
return '⏱ ' + (r.d / 1000).toFixed(1) + 's elapsed with the model, ⚡ ' + r.o + ' output tokens (' + tps.toFixed(1) + ' tok/s)';
|
|
206
248
|
}
|
|
207
249
|
function modelTotal(byModel, m) {
|
|
208
250
|
var b = byModel[m] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
@@ -234,6 +276,7 @@
|
|
|
234
276
|
function showTip(html, x, y) {
|
|
235
277
|
var t = tip();
|
|
236
278
|
t.innerHTML = html;
|
|
279
|
+
if (window.lucide) lucide.createIcons();
|
|
237
280
|
t.classList.add('show');
|
|
238
281
|
moveTip(x, y);
|
|
239
282
|
}
|
|
@@ -265,19 +308,104 @@
|
|
|
265
308
|
var denom = b.input + b.cacheRead;
|
|
266
309
|
var hit = denom ? (b.cacheRead / denom * 100).toFixed(1) + '%' : '-';
|
|
267
310
|
var total = b.input + b.output + b.cacheRead + b.cacheWrite;
|
|
268
|
-
var
|
|
269
|
-
h
|
|
270
|
-
h += '<div class="tr"><span class="sw" style="background:var(--
|
|
311
|
+
var cb = ((DATA.byModelBucketUsd || {})[m]) || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
312
|
+
var h = '<div class="tt">' + shortName(m) + '</div><div class="tv">' + fmtShort(total) + ' total (' + fxMoney(cb.input + cb.output + cb.cacheRead + cb.cacheWrite) + ')</div>';
|
|
313
|
+
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">input</span><span class="tvr">' + fmt(b.input) + ' (' + fxMoney(cb.input) + ')</span></div>';
|
|
314
|
+
h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">output</span><span class="tvr">' + fmt(b.output) + ' (' + fxMoney(cb.output) + ')</span></div>';
|
|
315
|
+
if (b.cacheRead > 0) h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache read</span><span class="tvr">' + fmt(b.cacheRead) + ' (' + fxMoney(cb.cacheRead) + ')</span></div>';
|
|
316
|
+
if (b.cacheWrite > 0) h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache write</span><span class="tvr">' + fmt(b.cacheWrite) + ' (' + fxMoney(cb.cacheWrite) + ')</span></div>';
|
|
271
317
|
h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">requests</span><span class="tvr">' + fmt(req) + '</span></div>';
|
|
272
318
|
h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache hit</span><span class="tvr">' + hit + '</span></div>';
|
|
273
319
|
return h;
|
|
274
320
|
}
|
|
321
|
+
function stampLocal(ts) {
|
|
322
|
+
var d = new Date(ts);
|
|
323
|
+
function p(n) { return String(n).padStart(2, '0'); }
|
|
324
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
|
|
325
|
+
}
|
|
326
|
+
// Per-request outcome. OMP records stopReason on every assistant turn and,
|
|
327
|
+
// on failure, the HTTP errorStatus + errorMessage; toolUse turns are
|
|
328
|
+
// ordinary completed turns, so only error and aborted stand apart.
|
|
329
|
+
var STATUS_RANK = { error: 0, aborted: 1, completed: 2 };
|
|
330
|
+
function statusRank(r) { var k = STATUS_RANK[r.st]; return k === undefined ? 2 : k; }
|
|
331
|
+
function statusLabel(r) {
|
|
332
|
+
if (r.st === 'error') return 'error' + (r.code ? ' ' + r.code : '');
|
|
333
|
+
return r.st === 'aborted' ? 'aborted' : 'completed';
|
|
334
|
+
}
|
|
335
|
+
function statusColor(r) { return r.st === 'error' ? '#f87171' : r.st === 'aborted' ? '#fbbf24' : 'var(--accent)'; }
|
|
336
|
+
// Cost precedence. A recorded charge > 0 is authoritative and shown bare.
|
|
337
|
+
// A recording of exactly 0 usually means a free/local provider (every row on
|
|
338
|
+
// a local model reports 0) and would blank the column, so those fall back to
|
|
339
|
+
// the modeled figure carrying the dashboard's usual "~" estimate marker.
|
|
340
|
+
// costRecorded() still reports the raw 0 so the tooltip can show both.
|
|
341
|
+
function costRecorded(r) { return typeof r.usd === 'number'; }
|
|
342
|
+
function costIsMeasured(r) { return costRecorded(r) && r.usd > 0; }
|
|
343
|
+
function displayCost(r) { return costIsMeasured(r) ? r.usd : (r.est || 0); }
|
|
344
|
+
|
|
345
|
+
function tipRecentHTML(r) {
|
|
346
|
+
var tps = r.d !== undefined && r.d > 0 ? r.o / (r.d / 1000) : 0;
|
|
347
|
+
var isEst = !costIsMeasured(r);
|
|
348
|
+
var h = '<div class="tt">' + esc(shortName(r.m)) + '</div><div class="tv">' + stampLocal(r.t) + '</div>';
|
|
349
|
+
h += '<div class="tr"><span class="sw" style="background:#fb923c"></span><span class="tn">input</span><span class="tvr">' + fmt(r.i) + '</span></div>';
|
|
350
|
+
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">output</span><span class="tvr">' + fmt(r.o) + '</span></div>';
|
|
351
|
+
if ((r.cr || 0) > 0) h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache read</span><span class="tvr">' + fmt(r.cr) + '</span></div>';
|
|
352
|
+
if ((r.cw || 0) > 0) h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache write</span><span class="tvr">' + fmt(r.cw) + '</span></div>';
|
|
353
|
+
h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">elapsed</span><span class="tvr">' + fmtDur(r.d) + '</span></div>';
|
|
354
|
+
h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">speed</span><span class="tvr">' + (r.d !== undefined ? (tps < 10 ? tps.toFixed(1) : Math.round(tps)) + ' tok/s' : '–') + '</span></div>';
|
|
355
|
+
h += '<div class="tr"><span class="sw" style="background:' + statusColor(r) + '"></span><span class="tn">status</span><span class="tvr">' + statusLabel(r) + '</span></div>';
|
|
356
|
+
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">cost ' + (isEst ? '(est.)' : '(measured)') + '</span><span class="tvr">' + (isEst ? '~' : '') + fxMoney(displayCost(r)) + '</span></div>';
|
|
357
|
+
if (costRecorded(r)) h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">charged</span><span class="tvr">' + fxMoney(r.usd) + '</span></div>';
|
|
358
|
+
if (r.note) h += '<div class="tnote">' + esc(r.note) + '</div>';
|
|
359
|
+
return h;
|
|
360
|
+
}
|
|
361
|
+
// Zone indicators: Lucide face per savings-bento metric. Coarse by design;
|
|
362
|
+
// captions say est. where modeled. Icons paint via showTip/render createIcons.
|
|
363
|
+
function zoneIcon(glyph, cls) { return '<i data-lucide="' + glyph + '" class="zface ' + cls + '"></i>'; }
|
|
364
|
+
function co2Zone(g) {
|
|
365
|
+
if (!(g > 0)) return ['minus', 'no data', ''];
|
|
366
|
+
if (g <= 50) return ['sprout', 'light', 'good'];
|
|
367
|
+
if (g <= 500) return ['smile', 'moderate', 'good'];
|
|
368
|
+
if (g <= 2000) return ['meh', 'heavy', 'warn'];
|
|
369
|
+
return ['flame', 'very high', 'bad'];
|
|
370
|
+
}
|
|
371
|
+
function levZone(x) {
|
|
372
|
+
if (!(x > 0)) return ['minus', 'no savings yet', ''];
|
|
373
|
+
if (x >= 3) return ['rocket', 'high leverage', 'good'];
|
|
374
|
+
if (x >= 1) return ['smile', 'solid', 'good'];
|
|
375
|
+
return ['meh', 'light', 'warn'];
|
|
376
|
+
}
|
|
377
|
+
function shareZone(p) {
|
|
378
|
+
if (!(p > 0)) return ['minus', 'uncached', ''];
|
|
379
|
+
if (p >= 80) return ['rocket', 'high', 'good'];
|
|
380
|
+
if (p >= 50) return ['smile', 'good', 'good'];
|
|
381
|
+
return ['meh', 'low', 'warn'];
|
|
382
|
+
}
|
|
275
383
|
function tipCo2HTML() {
|
|
384
|
+
var z = co2Zone(DATA.co2g || 0);
|
|
276
385
|
var h = '<div class="tt">CO2</div><div class="tv">~' + (DATA.co2g || 0).toFixed(1) + 'g est.</div>';
|
|
386
|
+
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">zone</span><span class="tvr">' + zoneIcon(z[0], z[2]) + ' ' + z[1] + '</span></div>';
|
|
277
387
|
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">energy</span><span class="tvr">~' + (DATA.energyWh || 0).toFixed(1) + ' Wh</span></div>';
|
|
278
|
-
h
|
|
279
|
-
|
|
280
|
-
|
|
388
|
+
return h;
|
|
389
|
+
}
|
|
390
|
+
function tipSavedHTML() {
|
|
391
|
+
var usd = DATA.usd || 0, saved = DATA.savedUsd || 0;
|
|
392
|
+
var lev = usd ? saved / usd : 0;
|
|
393
|
+
var z = levZone(lev);
|
|
394
|
+
var t = DATA.tokens || { cacheRead: 0 };
|
|
395
|
+
var h = '<div class="tt">Saved by cache</div><div class="tv">' + fxMoney(saved) + ' est.</div>';
|
|
396
|
+
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">zone</span><span class="tvr">' + zoneIcon(z[0], z[2]) + ' ' + z[1] + '</span></div>';
|
|
397
|
+
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">leverage</span><span class="tvr">x' + lev.toFixed(1) + ' per $1</span></div>';
|
|
398
|
+
h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache read</span><span class="tvr">' + fmtShort(t.cacheRead || 0) + '</span></div>';
|
|
399
|
+
return h;
|
|
400
|
+
}
|
|
401
|
+
function tipShareHTML(share, pp) {
|
|
402
|
+
var z = shareZone(share);
|
|
403
|
+
var t = DATA.tokens || { cacheRead: 0, cacheWrite: 0 };
|
|
404
|
+
var h = '<div class="tt">Cache share</div><div class="tv">' + share + '%</div>';
|
|
405
|
+
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">zone</span><span class="tvr">' + zoneIcon(z[0], z[2]) + ' ' + z[1] + '</span></div>';
|
|
406
|
+
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">read</span><span class="tvr">' + fmtShort(t.cacheRead || 0) + '</span></div>';
|
|
407
|
+
if ((t.cacheWrite || 0) > 0) h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">write</span><span class="tvr">' + fmtShort(t.cacheWrite) + '</span></div>';
|
|
408
|
+
h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">7d trend</span><span class="tvr">' + (pp >= 0 ? '+' : '') + pp.toFixed(1) + 'pp</span></div>';
|
|
281
409
|
return h;
|
|
282
410
|
}
|
|
283
411
|
function hoverModel(el, m) {
|
|
@@ -285,6 +413,11 @@
|
|
|
285
413
|
el.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
|
|
286
414
|
el.addEventListener('mouseleave', hideTip);
|
|
287
415
|
}
|
|
416
|
+
function hoverRecent(el, r) {
|
|
417
|
+
el.addEventListener('mouseenter', function(ev) { showTip(tipRecentHTML(r), ev.clientX, ev.clientY); });
|
|
418
|
+
el.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
|
|
419
|
+
el.addEventListener('mouseleave', hideTip);
|
|
420
|
+
}
|
|
288
421
|
function dayModelRows(key) {
|
|
289
422
|
var per = (DATA.byDayModel || {})[key] || {};
|
|
290
423
|
var tops = topModels(DATA.byModel || {}, 8), rows = [], other = 0;
|
|
@@ -444,33 +577,62 @@
|
|
|
444
577
|
});
|
|
445
578
|
}
|
|
446
579
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
580
|
+
var modelPage = 1, modelPer = 10, recentPage = 1, recentPer = 25;
|
|
581
|
+
// Recent requests default to newest-first, which is how the transcript
|
|
582
|
+
// arrives; every column is sortable like the command table.
|
|
583
|
+
var recentSort = { key: 'when', dir: -1 }, recentRows = [];
|
|
584
|
+
function recentVal(r, key) {
|
|
585
|
+
if (key === 'model') return r.m.toLowerCase();
|
|
586
|
+
if (key === 'input') return r.i;
|
|
587
|
+
if (key === 'output') return r.o;
|
|
588
|
+
if (key === 'time') return r.d === undefined ? null : r.d;
|
|
589
|
+
if (key === 'status') return statusRank(r);
|
|
590
|
+
if (key === 'cost') return displayCost(r);
|
|
591
|
+
return r.t;
|
|
592
|
+
}
|
|
593
|
+
function sortRecent() {
|
|
594
|
+
var k = recentSort.key, d = recentSort.dir;
|
|
595
|
+
recentRows.sort(function(a, b) {
|
|
596
|
+
var av = recentVal(a, k), bv = recentVal(b, k);
|
|
597
|
+
if (av === null || av === undefined) return 1;
|
|
598
|
+
if (bv === null || bv === undefined) return -1;
|
|
599
|
+
if (av < bv) return -d;
|
|
600
|
+
if (av > bv) return d;
|
|
601
|
+
return b.t - a.t;
|
|
602
|
+
});
|
|
603
|
+
Array.prototype.forEach.call(document.querySelectorAll('#recentTable .thsort'), function(btn) {
|
|
604
|
+
var on = btn.dataset.sort === k;
|
|
605
|
+
btn.querySelector('.arr').textContent = on ? (d === 1 ? '↑' : '↓') : '⇅';
|
|
606
|
+
btn.closest('th').setAttribute('aria-sort', on ? (d === 1 ? 'ascending' : 'descending') : 'none');
|
|
453
607
|
});
|
|
454
|
-
if (!prev) return cur ? 'new' : '-';
|
|
455
|
-
var p = Math.round((cur - prev) / prev * 100);
|
|
456
|
-
return (p >= 0 ? '+' : '') + p + '%';
|
|
457
608
|
}
|
|
458
609
|
function renderModels() {
|
|
459
610
|
var byModel = DATA.byModel || {};
|
|
460
|
-
var tops = topModels(byModel,
|
|
611
|
+
var tops = topModels(byModel, Object.keys(byModel).length).filter(function(m) { return modelTotal(byModel, m) > 0; });
|
|
461
612
|
var cards = document.getElementById('modelCards');
|
|
462
613
|
cards.innerHTML = '';
|
|
463
614
|
if (!tops.length) cards.innerHTML = '<div class="empty md:col-span-3">' + emptyState('boxes', 'No models yet', 'Model token totals will appear here once sessions report tokens.') + '</div>';
|
|
464
615
|
tops.slice(0, 3).forEach(function(m, i) {
|
|
465
|
-
var v = vendorOf(m)
|
|
616
|
+
var v = vendorOf(m);
|
|
466
617
|
var card = document.createElement('div');
|
|
467
618
|
card.className = 'modelcard relative p-4 rounded-xl overflow-hidden';
|
|
468
|
-
if (v.
|
|
619
|
+
if (v.svg) {
|
|
620
|
+
var ghostSvg = document.createElement('span');
|
|
621
|
+
ghostSvg.className = 'ghostimg'; ghostSvg.setAttribute('aria-hidden', 'true');
|
|
622
|
+
ghostSvg.innerHTML = v.svg;
|
|
623
|
+
card.appendChild(ghostSvg);
|
|
624
|
+
} else if (v.slug) {
|
|
469
625
|
var ghost = document.createElement('img');
|
|
470
626
|
ghost.className = 'ghostimg'; ghost.alt = '';
|
|
471
627
|
ghost.src = 'https://cdn.simpleicons.org/' + v.slug + '/white';
|
|
472
628
|
ghost.setAttribute('aria-hidden', 'true');
|
|
473
629
|
card.appendChild(ghost);
|
|
630
|
+
} else {
|
|
631
|
+
var ghostSparkle = document.createElement('i');
|
|
632
|
+
ghostSparkle.setAttribute('data-lucide', 'sparkles');
|
|
633
|
+
ghostSparkle.className = 'ghosticon';
|
|
634
|
+
ghostSparkle.setAttribute('aria-hidden', 'true');
|
|
635
|
+
card.appendChild(ghostSparkle);
|
|
474
636
|
}
|
|
475
637
|
card.style.cssText = 'border: 1px solid var(--line); background: var(--panel)';
|
|
476
638
|
var head = document.createElement('p');
|
|
@@ -488,13 +650,6 @@
|
|
|
488
650
|
var tot = document.createElement('p'); tot.className = 'mono font-bold text-lg shrink-0'; tot.textContent = fmtShort(modelTotal(byModel, m));
|
|
489
651
|
row.appendChild(badge); row.appendChild(mid); row.appendChild(tot);
|
|
490
652
|
card.appendChild(head); card.appendChild(row);
|
|
491
|
-
if (d !== 'new') {
|
|
492
|
-
var delta = document.createElement('p');
|
|
493
|
-
delta.className = 'mono text-xs mt-3';
|
|
494
|
-
delta.style.color = d.charAt(0) === '-' ? '#f87171' : 'var(--accent)';
|
|
495
|
-
delta.textContent = d;
|
|
496
|
-
card.appendChild(delta);
|
|
497
|
-
}
|
|
498
653
|
hoverModel(card, m);
|
|
499
654
|
cards.appendChild(card);
|
|
500
655
|
});
|
|
@@ -502,10 +657,13 @@
|
|
|
502
657
|
ol.innerHTML = '';
|
|
503
658
|
var max = 1;
|
|
504
659
|
tops.forEach(function(m) { max = Math.max(max, modelTotal(byModel, m)); });
|
|
505
|
-
|
|
660
|
+
var modelPages = Math.max(1, Math.ceil(tops.length / modelPer));
|
|
661
|
+
if (modelPage > modelPages) modelPage = modelPages;
|
|
662
|
+
var modelStart = (modelPage - 1) * modelPer;
|
|
663
|
+
tops.slice(modelStart, modelStart + modelPer).forEach(function(m, i) {
|
|
506
664
|
var v = vendorOf(m);
|
|
507
665
|
var li = document.createElement('li');
|
|
508
|
-
li.className = 'mrow flex items-center gap-3 px-4 py-3' + (i < tops.length - 1 ? ' rowline' : '');
|
|
666
|
+
li.className = 'mrow flex items-center gap-3 px-4 py-3' + (i < Math.min(tops.length - modelStart, modelPer) - 1 ? ' rowline' : '');
|
|
509
667
|
var badgeWrap = document.createElement('span');
|
|
510
668
|
badgeWrap.innerHTML = brandHTML(v, true);
|
|
511
669
|
var badge = badgeWrap.firstChild;
|
|
@@ -527,6 +685,7 @@
|
|
|
527
685
|
ol.appendChild(li);
|
|
528
686
|
});
|
|
529
687
|
document.getElementById('modelsCount').textContent = tops.length ? tops.length + ' models' : '';
|
|
688
|
+
paintPager('modelPages', 'modelRange', modelPage, modelPer, tops.length, function(p) { modelPage = p; renderModels(); });
|
|
530
689
|
ol.style.display = tops.length ? '' : 'none';
|
|
531
690
|
document.getElementById('emptyModels').classList.toggle('hidden', tops.length > 0);
|
|
532
691
|
if (tops.length && 'IntersectionObserver' in window && !reduce) {
|
|
@@ -553,8 +712,43 @@
|
|
|
553
712
|
|
|
554
713
|
// Union of session tool calls + RTK-metered commands, sortable + paged.
|
|
555
714
|
// Session tools were never metered per command: saved / avg / time show – and sort last.
|
|
556
|
-
|
|
557
|
-
|
|
715
|
+
// RTK rows arrive per (command, project) because rtk's history is machine-wide:
|
|
716
|
+
// fusing projects let one repo's `rtk grep` stand in for every project's.
|
|
717
|
+
var cmdSort = { key: 'count', dir: -1 }, cmdPage = 1, cmdPer = 15, cmdRows = [], cmdProject = '';
|
|
718
|
+
function cmdVal(r, key) {
|
|
719
|
+
if (key === 'name') return r.name.toLowerCase();
|
|
720
|
+
if (key === 'project') return (r.project || '').toLowerCase();
|
|
721
|
+
return r[key];
|
|
722
|
+
}
|
|
723
|
+
function baseName(p) {
|
|
724
|
+
if (!p) return '–';
|
|
725
|
+
var parts = String(p).replace(/\/+$/, '').split('/');
|
|
726
|
+
return parts[parts.length - 1] || p;
|
|
727
|
+
}
|
|
728
|
+
// Options come from the rows themselves so the list follows the machine.
|
|
729
|
+
// Repaint is skipped while the option set is unchanged: render runs every
|
|
730
|
+
// 5s and a rebuild would close an open dropdown mid-selection.
|
|
731
|
+
function paintProjectFilter(all) {
|
|
732
|
+
var sel = document.getElementById('cmdProject');
|
|
733
|
+
if (!sel) return;
|
|
734
|
+
var seen = {};
|
|
735
|
+
all.forEach(function(r) { if (r.project) seen[r.project] = 1; });
|
|
736
|
+
var keys = Object.keys(seen).sort();
|
|
737
|
+
if (cmdProject && keys.indexOf(cmdProject) < 0) cmdProject = '';
|
|
738
|
+
var sig = keys.join('\n') + '\u0000' + cmdProject;
|
|
739
|
+
if (sel.dataset.sig === sig) return;
|
|
740
|
+
sel.dataset.sig = sig;
|
|
741
|
+
sel.innerHTML = '';
|
|
742
|
+
var any = document.createElement('option');
|
|
743
|
+
any.value = ''; any.textContent = 'All projects (' + keys.length + ')';
|
|
744
|
+
sel.appendChild(any);
|
|
745
|
+
keys.forEach(function(k) {
|
|
746
|
+
var o = document.createElement('option');
|
|
747
|
+
o.value = k; o.textContent = baseName(k); o.title = k;
|
|
748
|
+
sel.appendChild(o);
|
|
749
|
+
});
|
|
750
|
+
sel.value = cmdProject;
|
|
751
|
+
}
|
|
558
752
|
function sortCmd() {
|
|
559
753
|
var k = cmdSort.key, d = cmdSort.dir;
|
|
560
754
|
cmdRows.sort(function(a, b) {
|
|
@@ -573,12 +767,14 @@
|
|
|
573
767
|
}
|
|
574
768
|
function renderCmd() {
|
|
575
769
|
var g = (DATA.rtkGain || {});
|
|
576
|
-
|
|
577
|
-
return { name: r[0], count: r[1], saved: null, avgPct: null, avgMs: null };
|
|
770
|
+
var all = (DATA.byTool || []).map(function(r) {
|
|
771
|
+
return { name: r[0], project: '', count: r[1], saved: null, avgPct: null, avgMs: null };
|
|
578
772
|
});
|
|
579
773
|
(g.byCommand || []).forEach(function(r) {
|
|
580
|
-
|
|
774
|
+
all.push({ name: r.command, project: r.project || '', count: r.count, saved: r.saved, avgPct: r.avgPct, avgMs: r.avgMs });
|
|
581
775
|
});
|
|
776
|
+
paintProjectFilter(all);
|
|
777
|
+
cmdRows = cmdProject ? all.filter(function(r) { return r.project === cmdProject; }) : all;
|
|
582
778
|
sortCmd();
|
|
583
779
|
var pages = Math.max(1, Math.ceil(cmdRows.length / cmdPer));
|
|
584
780
|
if (cmdPage > pages) cmdPage = pages;
|
|
@@ -593,6 +789,7 @@
|
|
|
593
789
|
tr.className = 'mrow' + (i < Math.min(cmdRows.length - start, cmdPer) - 1 ? ' rowline' : '');
|
|
594
790
|
tr.innerHTML = '<td class="text-right pr-3 py-2.5 mono text-xs w-10" style="color: var(--dim)"></td>' +
|
|
595
791
|
'<td class="py-2.5 pr-3 truncate" style="max-width: 280px"></td>' +
|
|
792
|
+
'<td class="py-2.5 pr-3 truncate" style="max-width: 120px; color: var(--dim)"></td>' +
|
|
596
793
|
'<td class="text-right py-2.5 pr-3 font-bold"></td>' +
|
|
597
794
|
'<td class="text-right py-2.5 pr-3 font-bold"></td>' +
|
|
598
795
|
'<td class="text-right py-2.5 pr-3" style="color: var(--accent)"></td>' +
|
|
@@ -602,11 +799,13 @@
|
|
|
602
799
|
tds[0].textContent = String(n + 1).padStart(2, '0');
|
|
603
800
|
tds[1].textContent = r.name;
|
|
604
801
|
tds[1].title = r.name;
|
|
605
|
-
tds[2].textContent =
|
|
606
|
-
tds[
|
|
607
|
-
tds[
|
|
608
|
-
tds[
|
|
609
|
-
tds[
|
|
802
|
+
tds[2].textContent = baseName(r.project);
|
|
803
|
+
tds[2].title = r.project || 'session tool call (no project recorded)';
|
|
804
|
+
tds[3].textContent = fmt(r.count);
|
|
805
|
+
tds[4].textContent = r.saved === null ? '–' : fmtShort(r.saved);
|
|
806
|
+
tds[5].textContent = r.avgPct === null ? '–' : r.avgPct.toFixed(1) + '%';
|
|
807
|
+
tds[6].textContent = r.avgMs === null || r.avgMs === undefined ? '–' : fmtMs(r.avgMs);
|
|
808
|
+
tds[7].firstChild.firstChild.style.width = r.count ? Math.round(r.count / max * 100) + '%' : '0';
|
|
610
809
|
body.appendChild(tr);
|
|
611
810
|
});
|
|
612
811
|
document.getElementById('cmdTable').style.display = cmdRows.length ? '' : 'none';
|
|
@@ -616,26 +815,31 @@
|
|
|
616
815
|
g.commands ? fmt(tools) + ' tools · ' + fmt(g.commands) + ' commands · ' + fmtShort(g.saved) + ' saved' : 'tool calls in sessions';
|
|
617
816
|
paintCmdPager(pages);
|
|
618
817
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
var
|
|
818
|
+
// Shared page-number painter: prev/next plus windowed numbers with
|
|
819
|
+
// ellipsis. onPick(page) rerenders the owning card. Same controls as the
|
|
820
|
+
// Command tools pager; Models and Recent reuse it verbatim.
|
|
821
|
+
function paintPager(wrapId, rangeId, page, per, total, onPick) {
|
|
822
|
+
var start = total ? (page - 1) * per + 1 : 0;
|
|
823
|
+
document.getElementById(rangeId).textContent =
|
|
824
|
+
'Showing ' + start + '–' + Math.min(total, page * per) + ' of ' + total;
|
|
825
|
+
var wrap = document.getElementById(wrapId);
|
|
624
826
|
wrap.innerHTML = '';
|
|
625
|
-
|
|
827
|
+
var pages = Math.max(1, Math.ceil(total / per));
|
|
828
|
+
if (page > pages) onPick(pages);
|
|
829
|
+
function btn(label, next, opts) {
|
|
626
830
|
var b = document.createElement('button');
|
|
627
831
|
b.type = 'button'; b.className = 'pgbtn mono' + (opts && opts.on ? ' on' : '');
|
|
628
832
|
b.textContent = label;
|
|
629
833
|
if (opts && opts.dim) b.setAttribute('aria-label', opts.dim);
|
|
630
834
|
if (opts && opts.off) b.disabled = true;
|
|
631
|
-
else b.addEventListener('click', function() {
|
|
835
|
+
else b.addEventListener('click', function() { onPick(next); });
|
|
632
836
|
wrap.appendChild(b);
|
|
633
837
|
return b;
|
|
634
838
|
}
|
|
635
|
-
btn('‹',
|
|
839
|
+
btn('‹', page - 1, { dim: 'Previous page', off: page <= 1 });
|
|
636
840
|
var nums = [];
|
|
637
841
|
for (var p = 1; p <= pages; p++) {
|
|
638
|
-
if (p === 1 || p === pages || Math.abs(p -
|
|
842
|
+
if (p === 1 || p === pages || Math.abs(p - page) <= 1) nums.push(p);
|
|
639
843
|
else if (nums[nums.length - 1] !== '…') nums.push('…');
|
|
640
844
|
}
|
|
641
845
|
nums.forEach(function(p) {
|
|
@@ -643,9 +847,27 @@
|
|
|
643
847
|
var s = document.createElement('span');
|
|
644
848
|
s.textContent = '…'; s.style.padding = '0 2px';
|
|
645
849
|
wrap.appendChild(s);
|
|
646
|
-
} else btn(String(p), p, { on: p ===
|
|
850
|
+
} else btn(String(p), p, { on: p === page });
|
|
851
|
+
});
|
|
852
|
+
btn('›', page + 1, { dim: 'Next page', off: page >= pages });
|
|
853
|
+
}
|
|
854
|
+
function paintCmdPager(pages) {
|
|
855
|
+
var total = cmdRows.length;
|
|
856
|
+
if (cmdPage > pages) cmdPage = pages;
|
|
857
|
+
paintPager('cmdPages', 'cmdRange', cmdPage, cmdPer, total, function(p) { cmdPage = p; renderCmd(); });
|
|
858
|
+
}
|
|
859
|
+
function bindPerPage(sel, get, set, render) {
|
|
860
|
+
Array.prototype.forEach.call(document.querySelectorAll(sel + ' [data-pp]'), function(btn) {
|
|
861
|
+
if (btn.dataset.bound) return;
|
|
862
|
+
btn.dataset.bound = '1';
|
|
863
|
+
btn.addEventListener('click', function() {
|
|
864
|
+
if (Number(btn.dataset.pp) === get()) return;
|
|
865
|
+
Array.prototype.forEach.call(document.querySelectorAll(sel + ' [data-pp]'), function(b) { b.classList.remove('on'); });
|
|
866
|
+
btn.classList.add('on');
|
|
867
|
+
set(Number(btn.dataset.pp));
|
|
868
|
+
render();
|
|
869
|
+
});
|
|
647
870
|
});
|
|
648
|
-
btn('›', cmdPage + 1, { dim: 'Next page', off: cmdPage >= pages });
|
|
649
871
|
}
|
|
650
872
|
function bindCmdTable() {
|
|
651
873
|
if (bindCmdTable.done) return;
|
|
@@ -659,17 +881,25 @@
|
|
|
659
881
|
renderCmd();
|
|
660
882
|
});
|
|
661
883
|
});
|
|
662
|
-
|
|
884
|
+
bindPerPage('#cmdPager', function() { return cmdPer; }, function(n) { cmdPer = n; cmdPage = 1; }, renderCmd);
|
|
885
|
+
var pf = document.getElementById('cmdProject');
|
|
886
|
+
if (pf) pf.addEventListener('change', function() { cmdProject = pf.value; cmdPage = 1; renderCmd(); });
|
|
887
|
+
bindPerPage('section[aria-label="All models"]', function() { return modelPer; }, function(n) { modelPer = n; modelPage = 1; }, renderModels);
|
|
888
|
+
bindPerPage('section[aria-label="Recent requests"]', function() { return recentPer; }, function(n) { recentPer = n; recentPage = 1; }, renderRecent);
|
|
889
|
+
}
|
|
890
|
+
function bindRecentTable() {
|
|
891
|
+
if (bindRecentTable.done) return;
|
|
892
|
+
bindRecentTable.done = true;
|
|
893
|
+
Array.prototype.forEach.call(document.querySelectorAll('#recentTable .thsort'), function(btn) {
|
|
663
894
|
btn.addEventListener('click', function() {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
895
|
+
var k = btn.dataset.sort;
|
|
896
|
+
if (recentSort.key === k) recentSort.dir = -recentSort.dir;
|
|
897
|
+
else { recentSort.key = k; recentSort.dir = k === 'model' || k === 'status' ? 1 : -1; }
|
|
898
|
+
recentPage = 1;
|
|
899
|
+
renderRecent();
|
|
669
900
|
});
|
|
670
901
|
});
|
|
671
902
|
}
|
|
672
|
-
|
|
673
903
|
function fmtMs(ms) {
|
|
674
904
|
if (ms < 1000) return Math.round(ms) + 'ms';
|
|
675
905
|
return (ms / 1000).toFixed(1) + 's';
|
|
@@ -783,7 +1013,19 @@
|
|
|
783
1013
|
if (pel && pair[1]) { pel.textContent = pair[1]; pel.title = pair[1]; }
|
|
784
1014
|
});
|
|
785
1015
|
countUp(document.getElementById('saved'), d.savedUsd || 0, true);
|
|
786
|
-
|
|
1016
|
+
(function() {
|
|
1017
|
+
var lev = (d.usd || 0) ? (d.savedUsd || 0) / d.usd : 0;
|
|
1018
|
+
var z = levZone(lev);
|
|
1019
|
+
var note = document.getElementById('usdNote');
|
|
1020
|
+
note.innerHTML = ((d.priced ? 'per-model price table' : 'incl. default pricing') + (fx.live ? ' · fx live' : ' · fx snapshot') + ' · ') + zoneIcon(z[0], z[2]) + ' ' + z[1];
|
|
1021
|
+
note.title = 'Cache leverage x' + lev.toFixed(1) + ' — cache savings per $1 spent, vs full input price';
|
|
1022
|
+
})();
|
|
1023
|
+
(function() {
|
|
1024
|
+
var z = co2Zone(d.co2g || 0);
|
|
1025
|
+
var el = document.getElementById('co2');
|
|
1026
|
+
el.innerHTML = '~' + (d.co2g || 0).toFixed(1) + 'g ' + zoneIcon(z[0], z[2]);
|
|
1027
|
+
el.title = z[1] + ' footprint (est.)';
|
|
1028
|
+
})();
|
|
787
1029
|
(function() {
|
|
788
1030
|
var el = document.getElementById('co2');
|
|
789
1031
|
var card = el ? el.closest('div.rounded-xl') : null;
|
|
@@ -793,16 +1035,39 @@
|
|
|
793
1035
|
card.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
|
|
794
1036
|
card.addEventListener('mouseleave', hideTip);
|
|
795
1037
|
})();
|
|
1038
|
+
(function() {
|
|
1039
|
+
var el = document.getElementById('saved');
|
|
1040
|
+
var card = el ? el.closest('div.rounded-xl') : null;
|
|
1041
|
+
if (!card || card.dataset.savetip) return;
|
|
1042
|
+
card.dataset.savetip = '1';
|
|
1043
|
+
card.addEventListener('mouseenter', function(ev) { showTip(tipSavedHTML(), ev.clientX, ev.clientY); });
|
|
1044
|
+
card.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
|
|
1045
|
+
card.addEventListener('mouseleave', hideTip);
|
|
1046
|
+
})();
|
|
796
1047
|
var share = total ? Math.round((t.cacheRead + t.cacheWrite) / total * 100) : 0;
|
|
797
|
-
|
|
1048
|
+
var shareZ = shareZone(share);
|
|
1049
|
+
document.getElementById('cacheShare').innerHTML = share + '% ' + zoneIcon(shareZ[0], shareZ[2]);
|
|
1050
|
+
document.getElementById('cacheShare').title = shareZ[1] + ' cache share';
|
|
798
1051
|
var shareOf = function(b) { var s = dayTotal(b); return s ? ((b.cacheRead || 0) + (b.cacheWrite || 0)) / s * 100 : 0; };
|
|
799
1052
|
var keys = Object.keys(byDay).sort(), cPct = 0, pPct = 0, cn = 0, pn = 0;
|
|
800
1053
|
keys.slice(-7).forEach(function(k) { cPct += shareOf(byDay[k]); cn++; });
|
|
801
1054
|
keys.slice(-14, -7).forEach(function(k) { pPct += shareOf(byDay[k]); pn++; });
|
|
802
1055
|
var pp = (cn ? cPct / cn : 0) - (pn ? pPct / pn : 0);
|
|
803
1056
|
var cd = document.getElementById('cacheDelta');
|
|
804
|
-
|
|
805
|
-
|
|
1057
|
+
var cs = document.getElementById('cacheShare');
|
|
1058
|
+
cs.dataset.share = String(share);
|
|
1059
|
+
cs.dataset.pp = pp.toFixed(1);
|
|
1060
|
+
(function() {
|
|
1061
|
+
var el = document.getElementById('cacheShare');
|
|
1062
|
+
var card = el ? el.closest('div.rounded-xl') : null;
|
|
1063
|
+
if (!card || card.dataset.sharetip) return;
|
|
1064
|
+
card.dataset.sharetip = '1';
|
|
1065
|
+
card.addEventListener('mouseenter', function(ev) {
|
|
1066
|
+
showTip(tipShareHTML(Number(el.dataset.share || 0), Number(el.dataset.pp || 0)), ev.clientX, ev.clientY);
|
|
1067
|
+
});
|
|
1068
|
+
card.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
|
|
1069
|
+
card.addEventListener('mouseleave', hideTip);
|
|
1070
|
+
})();
|
|
806
1071
|
|
|
807
1072
|
(function() {
|
|
808
1073
|
var el = document.getElementById('ticker');
|
|
@@ -821,6 +1086,7 @@
|
|
|
821
1086
|
renderStrip(t);
|
|
822
1087
|
bindGraphTabs();
|
|
823
1088
|
bindCmdTable();
|
|
1089
|
+
bindRecentTable();
|
|
824
1090
|
renderGraph(d.byDay || {});
|
|
825
1091
|
renderModels();
|
|
826
1092
|
renderRecent();
|
|
@@ -833,33 +1099,73 @@
|
|
|
833
1099
|
}
|
|
834
1100
|
observe();
|
|
835
1101
|
}
|
|
836
|
-
|
|
1102
|
+
// shadcn/ui badge: default/secondary/destructive/outline carry the meaning,
|
|
1103
|
+
// the label carries the detail. Built as DOM nodes so provider error notes
|
|
1104
|
+
// never reach innerHTML.
|
|
1105
|
+
function statusBadge(r) {
|
|
1106
|
+
var b = document.createElement('span');
|
|
1107
|
+
b.className = 'badge';
|
|
1108
|
+
if (r.st === 'error') {
|
|
1109
|
+
b.classList.add('destructive');
|
|
1110
|
+
b.textContent = statusLabel(r);
|
|
1111
|
+
b.title = 'error' + (r.code ? ' ' + r.code : '') + (r.note ? ' — ' + r.note : '');
|
|
1112
|
+
} else if (r.st === 'aborted') {
|
|
1113
|
+
b.classList.add('outline');
|
|
1114
|
+
b.textContent = 'aborted';
|
|
1115
|
+
b.title = r.note || 'Interrupted by user';
|
|
1116
|
+
} else {
|
|
1117
|
+
b.classList.add('secondary');
|
|
1118
|
+
b.textContent = 'completed';
|
|
1119
|
+
if (r.note) b.title = r.note;
|
|
1120
|
+
}
|
|
1121
|
+
return b;
|
|
1122
|
+
}
|
|
837
1123
|
function renderRecent() {
|
|
838
1124
|
var body = document.getElementById('recent');
|
|
839
1125
|
body.innerHTML = '';
|
|
840
|
-
var
|
|
1126
|
+
var all = DATA.recent || [];
|
|
1127
|
+
recentRows = all.slice();
|
|
1128
|
+
sortRecent();
|
|
1129
|
+
var recentPages = Math.max(1, Math.ceil(recentRows.length / recentPer));
|
|
1130
|
+
if (recentPage > recentPages) recentPage = recentPages;
|
|
1131
|
+
var recentStart = (recentPage - 1) * recentPer;
|
|
1132
|
+
var rows = recentRows.slice(recentStart, recentStart + recentPer);
|
|
841
1133
|
rows.forEach(function(r, i) {
|
|
842
1134
|
var v = vendorOf(r.m);
|
|
1135
|
+
var measured = costIsMeasured(r);
|
|
843
1136
|
var tr = document.createElement('tr');
|
|
844
|
-
tr.className = 'rrow' + (i <
|
|
845
|
-
|
|
846
|
-
|
|
1137
|
+
tr.className = 'rrow' + (i < Math.min(recentRows.length - recentStart, recentPer) - 1 ? ' rowline' : '');
|
|
1138
|
+
tr.innerHTML = '<td class="py-2.5 pr-3 truncate" style="min-width: 0"></td>' +
|
|
1139
|
+
'<td class="text-right py-2.5 pr-3 whitespace-nowrap" style="color:#fb923c"></td>' +
|
|
1140
|
+
'<td class="text-right py-2.5 pr-3 whitespace-nowrap" style="color:var(--accent)"></td>' +
|
|
1141
|
+
'<td class="text-right py-2.5 pr-3 whitespace-nowrap" style="color:var(--dim)">–</td>' +
|
|
1142
|
+
'<td class="py-2.5 pr-3 whitespace-nowrap"></td>' +
|
|
847
1143
|
'<td class="text-right py-2.5 pr-3 whitespace-nowrap"></td>' +
|
|
848
1144
|
'<td class="text-right py-2.5 whitespace-nowrap" style="color: var(--dim)"></td>';
|
|
849
1145
|
var tds = tr.children;
|
|
1146
|
+
var dot = '<span style="display:inline-block;width:8px;height:8px;border-radius:99px;background:' + v.color + ';margin-right:8px"></span>';
|
|
850
1147
|
tds[0].innerHTML = dot + '<span></span>';
|
|
851
1148
|
tds[0].querySelector('span:last-child').textContent = r.m;
|
|
852
|
-
tds[
|
|
853
|
-
tds[
|
|
854
|
-
tds[
|
|
855
|
-
tds[
|
|
856
|
-
tds[
|
|
857
|
-
tds[
|
|
1149
|
+
tds[1].textContent = fmt(r.i);
|
|
1150
|
+
tds[2].textContent = fmt(r.o);
|
|
1151
|
+
tds[3].textContent = speedText(r);
|
|
1152
|
+
tds[4].appendChild(statusBadge(r));
|
|
1153
|
+
tds[5].textContent = (measured ? '' : '~') + fxMoney(displayCost(r));
|
|
1154
|
+
tds[5].title = measured
|
|
1155
|
+
? 'measured — charged by the provider'
|
|
1156
|
+
: (costRecorded(r)
|
|
1157
|
+
? 'est. — provider charged ' + fxMoney(r.usd) + ' (free or local), modeled from tokens'
|
|
1158
|
+
: 'est. — modeled from tokens');
|
|
1159
|
+
if (measured) tds[5].style.color = 'var(--ink)';
|
|
1160
|
+
tds[6].textContent = whenStamp(r.t);
|
|
1161
|
+
tds[6].title = stampLocal(r.t);
|
|
1162
|
+
hoverRecent(tr, r);
|
|
858
1163
|
body.appendChild(tr);
|
|
859
1164
|
});
|
|
860
|
-
document.getElementById('recentCount').textContent =
|
|
861
|
-
|
|
862
|
-
document.getElementById('
|
|
1165
|
+
document.getElementById('recentCount').textContent = all.length ? recentRows.length + ' requests' : '';
|
|
1166
|
+
paintPager('recentPages', 'recentRange', recentPage, recentPer, recentRows.length, function(p) { recentPage = p; renderRecent(); });
|
|
1167
|
+
document.getElementById('recentTable').style.display = all.length ? '' : 'none';
|
|
1168
|
+
document.getElementById('emptyRecent').classList.toggle('hidden', all.length > 0);
|
|
863
1169
|
}
|
|
864
1170
|
|
|
865
1171
|
var seen = new WeakSet();
|