@krtclcdy/tersio 2.17.0 → 2.19.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 +20 -4
- package/cli/common.js +8 -2
- package/cli/currency.js +102 -0
- package/cli/dashboard.js +54 -2
- package/cli/doctor.js +2 -0
- package/cli/install.js +7 -73
- package/cli/profile.js +92 -0
- package/cli/reset.js +4 -2
- package/cli/settings.js +122 -0
- package/cli/usage.js +42 -16
- package/dashboard/app.js +435 -67
- package/dashboard/styles.css +83 -3
- package/dashboard/template.html +97 -17
- 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 +13 -1
- package/extensions/shared/session-state.js +15 -0
- package/extensions/shared/usage-ledger.js +155 -83
- package/extensions/shared/usage-store.js +294 -0
- package/package.json +18 -1
- package/tersio.js +8 -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) {
|
|
@@ -100,6 +116,14 @@
|
|
|
100
116
|
function setCur(k) {
|
|
101
117
|
fx.cur = k; active = keys.indexOf(k);
|
|
102
118
|
try { localStorage.setItem('tersio-fx-cur', fx.cur); } catch (e) { }
|
|
119
|
+
// Served mode: also persist server-side so close → reopen keeps the
|
|
120
|
+
// choice. Every `tersio gain` run is a fresh ephemeral port (a new
|
|
121
|
+
// origin), so localStorage alone cannot survive a restart.
|
|
122
|
+
if (window.location.protocol !== 'file:' && typeof fetch === 'function') {
|
|
123
|
+
try {
|
|
124
|
+
fetch('currency', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ currency: k }) }).catch(function() { });
|
|
125
|
+
} catch (e) { }
|
|
126
|
+
}
|
|
103
127
|
paint();
|
|
104
128
|
if (DATA) render(DATA);
|
|
105
129
|
}
|
|
@@ -177,7 +201,7 @@
|
|
|
177
201
|
[/nemotron|nvidia/i, 'NVIDIA', 'nvidia', '#76b900'],
|
|
178
202
|
[/mistral/i, 'Mistral', 'mistralai', '#ff7000'],
|
|
179
203
|
[/claude|anthropic/i, 'Anthropic', 'anthropic', '#d97757'],
|
|
180
|
-
[/gemini|google/i, 'Google', '
|
|
204
|
+
[/gemini|google|gemma/i, 'Google', 'google', '#4285F4'],
|
|
181
205
|
];
|
|
182
206
|
function vendorOf(model) {
|
|
183
207
|
for (var i = 0; i < PROVIDERS.length; i++) {
|
|
@@ -205,12 +229,12 @@
|
|
|
205
229
|
function dayKey(d) {
|
|
206
230
|
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
207
231
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
232
|
+
// Absolute local stamp for the When column: "Sep 18, 11:14:08 PM". The
|
|
233
|
+
// column is sortable, and a relative label made that ordering unreadable.
|
|
234
|
+
function whenStamp(ts) {
|
|
235
|
+
var d = new Date(ts);
|
|
236
|
+
return d.toLocaleString('en-US', { month: 'short', day: 'numeric' }) + ', ' +
|
|
237
|
+
d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', second: '2-digit' });
|
|
214
238
|
}
|
|
215
239
|
// Per-request generation time + output throughput from the message-level
|
|
216
240
|
// `duration` (ms). Missing → '–'. Throughput uses output tokens since
|
|
@@ -234,13 +258,29 @@
|
|
|
234
258
|
var b = byModel[m] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
235
259
|
return b.input + b.output + b.cacheRead + b.cacheWrite;
|
|
236
260
|
}
|
|
237
|
-
|
|
238
|
-
var DATA = null, RANGE = 'daily', cardsShown = false;
|
|
239
|
-
|
|
261
|
+
var DATA = null, cardsShown = false;
|
|
240
262
|
function topModels(byModel, n) {
|
|
241
263
|
return Object.keys(byModel).sort(function(a, b) { return modelTotal(byModel, b) - modelTotal(byModel, a); }).slice(0, n);
|
|
242
264
|
}
|
|
243
|
-
|
|
265
|
+
// Display form for folded keys: namespace stays lowercase, model segments
|
|
266
|
+
// title-case with version dots kept (`deepseek-v4.1-flash` →
|
|
267
|
+
// `Deepseek-V4.1-Flash`). Keys arrive folded, so this prettifies only.
|
|
268
|
+
function displayModel(m) {
|
|
269
|
+
var bare = String(m).replace(/(?::free|-free)$/i, '');
|
|
270
|
+
function cap(s) {
|
|
271
|
+
var low = s.toLowerCase();
|
|
272
|
+
if (low === 'openai') return 'OpenAI';
|
|
273
|
+
if (low === 'ai') return 'AI';
|
|
274
|
+
if (/^\d+[a-z]+$/.test(low)) return low.toUpperCase();
|
|
275
|
+
if (s.length <= 2) return s.toUpperCase();
|
|
276
|
+
return s[0].toUpperCase() + s.slice(1).toLowerCase();
|
|
277
|
+
}
|
|
278
|
+
function seg(s) { return s.split('.').map(cap).join('.'); }
|
|
279
|
+
function words(s) { return s.split(/[-_:]+/).filter(Boolean).map(seg).join('-'); }
|
|
280
|
+
var slash = bare.indexOf('/');
|
|
281
|
+
if (slash >= 0) return bare.slice(0, slash).toLowerCase() + '/' + words(bare.slice(slash + 1));
|
|
282
|
+
return words(bare);
|
|
283
|
+
}
|
|
244
284
|
function allDays() {
|
|
245
285
|
var set = {};
|
|
246
286
|
Object.keys(DATA.byDay || {}).forEach(function(d) { set[d] = 1; });
|
|
@@ -277,7 +317,7 @@
|
|
|
277
317
|
var dt = new Date(key + 'T12:00:00');
|
|
278
318
|
return isNaN(dt) ? key : dt.toLocaleString('en-US', { month: 'short', day: 'numeric' }).toUpperCase();
|
|
279
319
|
}
|
|
280
|
-
function shortName(m) { return
|
|
320
|
+
function shortName(m) { var d = displayModel(m); return d.length > 22 ? d.slice(0, 21) + '...' : d; }
|
|
281
321
|
function tipDayHTML(key, total, rows) {
|
|
282
322
|
var h = '<div class="tt">' + dateHead(key) + '</div><div class="tv">' + fmtShort(total) + ' total</div>';
|
|
283
323
|
rows.forEach(function(r) {
|
|
@@ -307,15 +347,39 @@
|
|
|
307
347
|
function p(n) { return String(n).padStart(2, '0'); }
|
|
308
348
|
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
|
|
309
349
|
}
|
|
350
|
+
// Per-request outcome. OMP records stopReason on every assistant turn and,
|
|
351
|
+
// on failure, the HTTP errorStatus + errorMessage; toolUse turns are
|
|
352
|
+
// ordinary completed turns, so only error and aborted stand apart.
|
|
353
|
+
var STATUS_RANK = { error: 0, aborted: 1, completed: 2 };
|
|
354
|
+
function statusRank(r) { var k = STATUS_RANK[r.st]; return k === undefined ? 2 : k; }
|
|
355
|
+
function statusLabel(r) {
|
|
356
|
+
if (r.st === 'error') return 'error' + (r.code ? ' ' + r.code : '');
|
|
357
|
+
return r.st === 'aborted' ? 'aborted' : 'completed';
|
|
358
|
+
}
|
|
359
|
+
function statusColor(r) { return r.st === 'error' ? '#f87171' : r.st === 'aborted' ? '#fbbf24' : 'var(--accent)'; }
|
|
360
|
+
// Cost precedence. A recorded charge > 0 is authoritative and shown bare.
|
|
361
|
+
// A recording of exactly 0 usually means a free/local provider (every row on
|
|
362
|
+
// a local model reports 0) and would blank the column, so those fall back to
|
|
363
|
+
// the modeled figure carrying the dashboard's usual "~" estimate marker.
|
|
364
|
+
// costRecorded() still reports the raw 0 so the tooltip can show both.
|
|
365
|
+
function costRecorded(r) { return typeof r.usd === 'number'; }
|
|
366
|
+
function costIsMeasured(r) { return costRecorded(r) && r.usd > 0; }
|
|
367
|
+
function displayCost(r) { return costIsMeasured(r) ? r.usd : (r.est || 0); }
|
|
368
|
+
|
|
310
369
|
function tipRecentHTML(r) {
|
|
311
370
|
var tps = r.d !== undefined && r.d > 0 ? r.o / (r.d / 1000) : 0;
|
|
312
|
-
var
|
|
371
|
+
var isEst = !costIsMeasured(r);
|
|
372
|
+
var h = '<div class="tt">' + esc(shortName(r.m)) + '</div><div class="tv">' + stampLocal(r.t) + '</div>';
|
|
313
373
|
h += '<div class="tr"><span class="sw" style="background:#fb923c"></span><span class="tn">input</span><span class="tvr">' + fmt(r.i) + '</span></div>';
|
|
314
374
|
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>';
|
|
315
|
-
if ((r.cr || 0) > 0) h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache</span><span class="tvr">' +
|
|
316
|
-
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">' +
|
|
375
|
+
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>';
|
|
376
|
+
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>';
|
|
317
377
|
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>';
|
|
318
378
|
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>';
|
|
379
|
+
h += '<div class="tr"><span class="sw" style="background:' + statusColor(r) + '"></span><span class="tn">status</span><span class="tvr">' + statusLabel(r) + '</span></div>';
|
|
380
|
+
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>';
|
|
381
|
+
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>';
|
|
382
|
+
if (r.note) h += '<div class="tnote">' + esc(r.note) + '</div>';
|
|
319
383
|
return h;
|
|
320
384
|
}
|
|
321
385
|
// Zone indicators: Lucide face per savings-bento metric. Coarse by design;
|
|
@@ -378,6 +442,222 @@
|
|
|
378
442
|
el.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
|
|
379
443
|
el.addEventListener('mouseleave', hideTip);
|
|
380
444
|
}
|
|
445
|
+
// Model detail dialog: shadcn chart language over daily model volume.
|
|
446
|
+
// byDayModel is keyed by DAY (Record<day, Record<model, tokens>>); the old
|
|
447
|
+
// mdSeries read it keyed by model and rendered all-zero charts. Area chart
|
|
448
|
+
// with gradient + hover crosshair, downsampled rounded bars, token-mix
|
|
449
|
+
// donut, KPI strip, 30/90/ALL range tabs.
|
|
450
|
+
var mdModel = null, mdSpan = 'all', mdHover = { pts: [], days: [] };
|
|
451
|
+
function mdSeries(m) {
|
|
452
|
+
var days = allDays();
|
|
453
|
+
var per = DATA.byDayModel || {};
|
|
454
|
+
return days.map(function(d) { return { day: d, v: ((per[d] || {})[m] || 0) }; });
|
|
455
|
+
}
|
|
456
|
+
function mdSlice(series) {
|
|
457
|
+
if (mdSpan === 'all' || series.length <= mdSpan) return series;
|
|
458
|
+
return series.slice(series.length - mdSpan);
|
|
459
|
+
}
|
|
460
|
+
function mdMonthTicks(days) {
|
|
461
|
+
if (!days.length) return [];
|
|
462
|
+
var seen = {}, out = [];
|
|
463
|
+
days.forEach(function(d) {
|
|
464
|
+
var t = d.slice(0, 7);
|
|
465
|
+
if (!seen[t]) { seen[t] = 1; out.push({ day: d, label: new Date(d + 'T12:00:00').toLocaleString('en-US', { month: 'short' }) }); }
|
|
466
|
+
});
|
|
467
|
+
return out.slice(-6);
|
|
468
|
+
}
|
|
469
|
+
function mdShortDay(d) { return new Date(d + 'T12:00:00').toLocaleString('en-US', { month: 'short', day: 'numeric' }); }
|
|
470
|
+
function mdAreaChart(svg, pts, days) {
|
|
471
|
+
var W = 560, H = 190, max = 1, i, x, y;
|
|
472
|
+
pts.forEach(function(p) { max = Math.max(max, p); });
|
|
473
|
+
var step = pts.length > 1 ? (W - 16) / (pts.length - 1) : 0;
|
|
474
|
+
function X(i) { return 8 + i * step; }
|
|
475
|
+
function Y(p) { return 14 + (1 - p / max) * (H - 36); }
|
|
476
|
+
var grid = '';
|
|
477
|
+
for (i = 1; i <= 3; i++) { y = 14 + (H - 36) * i / 3; grid += '<line class="grid" x1="8" y1="' + y.toFixed(1) + '" x2="' + (W - 8) + '" y2="' + y.toFixed(1) + '"/>'; }
|
|
478
|
+
var d = pts.map(function(p, i) { return (i ? 'L' : 'M') + X(i).toFixed(1) + ' ' + Y(p).toFixed(1); }).join(' ');
|
|
479
|
+
var last = pts.length ? { x: X(pts.length - 1), y: Y(pts[pts.length - 1]) } : { x: 8, y: H - 22 };
|
|
480
|
+
svg.innerHTML = '<defs><linearGradient id="mdGrad" x1="0" y1="0" x2="0" y2="1">' +
|
|
481
|
+
'<stop offset="0" style="stop-color:var(--accent)" stop-opacity="0.35"/>' +
|
|
482
|
+
'<stop offset="1" style="stop-color:var(--accent)" stop-opacity="0"/></linearGradient></defs>' +
|
|
483
|
+
grid +
|
|
484
|
+
(pts.length ? '<path class="area" d="' + d + ' L' + last.x.toFixed(1) + ' ' + (H - 22) + ' L8 ' + (H - 22) + ' Z"/>' : '') +
|
|
485
|
+
(pts.length ? '<path class="line" d="' + d + '"/>' : '') +
|
|
486
|
+
(pts.length ? '<circle class="dot" cx="' + last.x.toFixed(1) + '" cy="' + last.y.toFixed(1) + '" r="4"/>' : '') +
|
|
487
|
+
(pts.length ? '<circle class="hoverdot" id="mdHoverDot" cx="-10" cy="-10" r="4" style="display:none"/>' : '');
|
|
488
|
+
mdHover = { pts: pts, days: days };
|
|
489
|
+
svg.onmousemove = function(ev) {
|
|
490
|
+
var tip = document.getElementById('mdTip');
|
|
491
|
+
if (!tip || !mdHover.pts.length) return;
|
|
492
|
+
var r = svg.getBoundingClientRect();
|
|
493
|
+
var fx = (ev.clientX - r.left) / r.width * W;
|
|
494
|
+
var idx = Math.max(0, Math.min(mdHover.pts.length - 1, Math.round((fx - 8) / (step || 1))));
|
|
495
|
+
var dot = document.getElementById('mdHoverDot');
|
|
496
|
+
if (dot) { dot.setAttribute('cx', X(idx).toFixed(1)); dot.setAttribute('cy', Y(mdHover.pts[idx]).toFixed(1)); dot.style.display = ''; }
|
|
497
|
+
tip.textContent = mdShortDay(mdHover.days[idx]) + ' · ' + fmtShort(mdHover.pts[idx]);
|
|
498
|
+
tip.style.display = 'block';
|
|
499
|
+
// Raw % positioning overflows past the dialog edge on the first/last
|
|
500
|
+
// points (the dialog clips it). Measure and pin inside the card.
|
|
501
|
+
var chart = svg.closest ? svg.closest('.md-chart') : null;
|
|
502
|
+
var px = svg.offsetLeft + (X(idx) / W) * svg.clientWidth;
|
|
503
|
+
if (chart) {
|
|
504
|
+
var half = tip.offsetWidth / 2;
|
|
505
|
+
px = Math.max(half + 4, Math.min(chart.clientWidth - half - 4, px));
|
|
506
|
+
}
|
|
507
|
+
tip.style.left = px + 'px';
|
|
508
|
+
tip.style.top = '8px';
|
|
509
|
+
};
|
|
510
|
+
svg.onmouseleave = function() {
|
|
511
|
+
var tip = document.getElementById('mdTip');
|
|
512
|
+
if (tip) tip.style.display = 'none';
|
|
513
|
+
var dot = document.getElementById('mdHoverDot');
|
|
514
|
+
if (dot) dot.style.display = 'none';
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function mdBars(el, vals, days) {
|
|
518
|
+
el.innerHTML = '';
|
|
519
|
+
var max = 1, i, total = 0, active = 0, peak = 0, peakI = 0;
|
|
520
|
+
vals.forEach(function(v, k) { max = Math.max(max, v); total += v; if (v > 0) { active++; if (v > peak) { peak = v; peakI = k; } } });
|
|
521
|
+
var n = vals.length, bucket = 1;
|
|
522
|
+
if (n > 90) bucket = Math.ceil(n / 90);
|
|
523
|
+
for (i = 0; i < n; i += bucket) {
|
|
524
|
+
var sum = 0, c = 0;
|
|
525
|
+
for (var j = i; j < Math.min(n, i + bucket); j++) { sum += vals[j]; c++; }
|
|
526
|
+
var avg = sum / c;
|
|
527
|
+
var s = document.createElement('span');
|
|
528
|
+
if (!avg) s.className = 'zero';
|
|
529
|
+
else if (avg === max) s.className = 'top';
|
|
530
|
+
s.style.height = Math.max(avg ? 5 : 2, Math.round(avg / max * 100)) + '%';
|
|
531
|
+
s.title = mdShortDay(days[i]) + ' · ' + fmtShort(Math.round(sum));
|
|
532
|
+
el.appendChild(s);
|
|
533
|
+
}
|
|
534
|
+
// Sparse models leave the bars mostly flat: name the peak day, its share
|
|
535
|
+
// of the period, and the per-active-day average instead of empty space.
|
|
536
|
+
var note = document.getElementById('mdBarsNote');
|
|
537
|
+
if (note) {
|
|
538
|
+
if (!active) note.textContent = 'no activity in range';
|
|
539
|
+
else note.textContent = 'Peak ' + mdShortDay(days[peakI]) + ' · ' + fmtShort(peak) + ' (' + (total ? Math.round(peak / total * 100) : 0) + '% of period) · avg ' + fmtShort(Math.round(total / active)) + ' / active day';
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function mdDonut(svg, parts) {
|
|
543
|
+
var r = 54, C = 2 * Math.PI * r, off = 0;
|
|
544
|
+
var total = parts.reduce(function(a, p) { return a + p.v; }, 0) || 1;
|
|
545
|
+
var html = '<circle class="tk" cx="70" cy="70" r="' + r + '"/>';
|
|
546
|
+
parts.forEach(function(p) {
|
|
547
|
+
var len = p.v / total * C;
|
|
548
|
+
if (len <= 0) return;
|
|
549
|
+
html += '<circle cx="70" cy="70" r="' + r + '" style="stroke:' + p.color + '" stroke-dasharray="' + len.toFixed(1) + ' ' + (C - len).toFixed(1) + '" stroke-dashoffset="' + (-off).toFixed(1) + '" stroke-linecap="butt"/>';
|
|
550
|
+
off += len;
|
|
551
|
+
});
|
|
552
|
+
svg.innerHTML = html;
|
|
553
|
+
document.getElementById('mdMixTotal').textContent = fmtShort(total);
|
|
554
|
+
document.getElementById('mdMixLegend').innerHTML = parts.filter(function(p) { return p.v > 0; }).map(function(p) {
|
|
555
|
+
return '<div class="row"><span class="dot" style="background:' + p.color + '"></span><span>' + p.label + '</span><span class="pct">' + (p.v / total * 100).toFixed(1) + '% · ' + fmtShort(p.v) + '</span></div>';
|
|
556
|
+
}).join('');
|
|
557
|
+
}
|
|
558
|
+
function mdMonths(el, days) {
|
|
559
|
+
el.innerHTML = '';
|
|
560
|
+
mdMonthTicks(days).forEach(function(t) {
|
|
561
|
+
var s = document.createElement('span');
|
|
562
|
+
s.textContent = t.label;
|
|
563
|
+
el.appendChild(s);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
function mdKpi(k, v, d, up) {
|
|
567
|
+
return '<div class="md-kpi"><p class="k mono">' + k + '</p><p class="v">' + v + '</p><p class="d mono ' + (up ? 'up' : 'flat') + '">' + d + '</p></div>';
|
|
568
|
+
}
|
|
569
|
+
function mdStat(k, v, s) {
|
|
570
|
+
return '<div class="md-stat"><p class="k mono">' + k + '</p><p class="v mono">' + v + '</p>' + (s ? '<p class="s mono">' + s + '</p>' : '') + '</div>';
|
|
571
|
+
}
|
|
572
|
+
function mdRange(days) {
|
|
573
|
+
if (!days.length) return '–';
|
|
574
|
+
return mdShortDay(days[0]).toUpperCase() + ' → ' + mdShortDay(days[days.length - 1]).toUpperCase();
|
|
575
|
+
}
|
|
576
|
+
function mdTrendText(vals) {
|
|
577
|
+
var max = 0, i;
|
|
578
|
+
vals.forEach(function(x) { max = Math.max(max, x); });
|
|
579
|
+
if (vals.length < 14) return max ? { t: 'peak ' + fmtShort(max) + ' in a day', up: true } : { t: 'no activity yet', up: false };
|
|
580
|
+
var a = 0, b = 0;
|
|
581
|
+
for (i = vals.length - 14; i < vals.length; i++) a += vals[i];
|
|
582
|
+
for (i = Math.max(0, vals.length - 28); i < vals.length - 14; i++) b += vals[i];
|
|
583
|
+
if (!b) return { t: fmtShort(a) + ' / 2 wks', up: a > 0 };
|
|
584
|
+
var pct = (a - b) / b * 100;
|
|
585
|
+
return { t: (pct >= 0 ? '+' : '') + pct.toFixed(0) + '% vs prior 2 wks', up: pct >= 0 };
|
|
586
|
+
}
|
|
587
|
+
function renderModelDialog(m) {
|
|
588
|
+
var byModel = DATA.byModel || {};
|
|
589
|
+
var tops = topModels(byModel, Object.keys(byModel).length);
|
|
590
|
+
var rank = tops.indexOf(m) + 1;
|
|
591
|
+
var b = byModel[m] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
592
|
+
var total = modelTotal(byModel, m);
|
|
593
|
+
var grand = 1;
|
|
594
|
+
tops.forEach(function(k) { grand += modelTotal(byModel, k); });
|
|
595
|
+
var v = vendorOf(m);
|
|
596
|
+
var req = ((DATA.byModelMessages || {})[m]) || 0;
|
|
597
|
+
var usd = ((DATA.byModelUsd || {})[m]) || 0;
|
|
598
|
+
var denom = b.input + b.cacheRead;
|
|
599
|
+
var hit = denom ? (b.cacheRead / denom * 100) : 0;
|
|
600
|
+
var series = mdSlice(mdSeries(m));
|
|
601
|
+
var days = series.map(function(p) { return p.day; });
|
|
602
|
+
var vals = series.map(function(p) { return p.v; });
|
|
603
|
+
var active = vals.filter(function(x) { return x > 0; }).length;
|
|
604
|
+
var tr = mdTrendText(mdSeries(m).map(function(p) { return p.v; }));
|
|
605
|
+
document.getElementById('mdBadge').innerHTML = brandHTML(v, false);
|
|
606
|
+
var title = document.getElementById('mdTitle');
|
|
607
|
+
title.textContent = displayModel(m);
|
|
608
|
+
title.title = m;
|
|
609
|
+
document.getElementById('mdSub').textContent = v.name + ' · ' + fmt(req) + ' requests · ' + (total / grand * 100).toFixed(1) + '% of volume';
|
|
610
|
+
document.getElementById('mdRank').textContent = rank ? '#' + String(rank).padStart(2, '0') : '#–';
|
|
611
|
+
var trend = document.getElementById('mdTrend');
|
|
612
|
+
trend.textContent = tr.t;
|
|
613
|
+
trend.className = 'md-trend mono' + (tr.up ? ' up' : '');
|
|
614
|
+
document.getElementById('mdKpis').innerHTML =
|
|
615
|
+
mdKpi('Tokens', fmtShort(total), mdRange(days), true) +
|
|
616
|
+
mdKpi('Spend', fxMoney(usd), req ? fxMoney(usd / req) + ' / req' : 'no requests', usd > 0) +
|
|
617
|
+
mdKpi('Cache hit', hit.toFixed(0) + '%', fmtShort(b.cacheRead) + ' cached', hit >= 50) +
|
|
618
|
+
mdKpi('Requests', fmt(req), active + ' active days', active > 0);
|
|
619
|
+
document.getElementById('mdMomRange').textContent = mdRange(days);
|
|
620
|
+
mdAreaChart(document.getElementById('mdSpark'), vals, days);
|
|
621
|
+
mdMonths(document.getElementById('mdMonths'), days);
|
|
622
|
+
var act = document.getElementById('mdActive');
|
|
623
|
+
act.textContent = active + ' / ' + days.length + ' days';
|
|
624
|
+
act.className = 'md-trend mono' + (active ? ' up' : '');
|
|
625
|
+
mdBars(document.getElementById('mdBars'), vals, days);
|
|
626
|
+
mdMonths(document.getElementById('mdBarsMonths'), days);
|
|
627
|
+
mdDonut(document.getElementById('mdMix'), [
|
|
628
|
+
{ label: 'Input', v: b.input, color: 'var(--accent)' },
|
|
629
|
+
{ label: 'Output', v: b.output, color: '#818cf8' },
|
|
630
|
+
{ label: 'Cache read', v: b.cacheRead, color: '#22d3ee' },
|
|
631
|
+
{ label: 'Cache write', v: b.cacheWrite, color: 'var(--dim)' },
|
|
632
|
+
]);
|
|
633
|
+
document.getElementById('mdStats').innerHTML =
|
|
634
|
+
mdStat('Input', fmtShort(b.input), null) +
|
|
635
|
+
mdStat('Output', fmtShort(b.output), null) +
|
|
636
|
+
mdStat('Cache read', fmtShort(b.cacheRead), hit.toFixed(0) + '% hit') +
|
|
637
|
+
mdStat('Cache write', fmtShort(b.cacheWrite), null);
|
|
638
|
+
if (window.lucide) lucide.createIcons();
|
|
639
|
+
if (hasGsap && !reduce) {
|
|
640
|
+
gsap.fromTo('#modelDialog .md-kpi', { y: 12, opacity: 0.2 }, { y: 0, opacity: 1, duration: 0.4, ease: 'power2.out', stagger: 0.05, overwrite: true });
|
|
641
|
+
gsap.fromTo('#modelDialog .md-chart', { y: 14, opacity: 0.2 }, { y: 0, opacity: 1, duration: 0.45, ease: 'power2.out', stagger: 0.07, overwrite: true });
|
|
642
|
+
gsap.fromTo('#modelDialog .md-stat', { scale: 0.94, opacity: 0.2 }, { scale: 1, opacity: 1, duration: 0.4, ease: 'power2.out', stagger: 0.04, overwrite: true });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function openModelDialog(m) {
|
|
646
|
+
var dlg = document.getElementById('modelDialog');
|
|
647
|
+
if (!dlg || typeof dlg.showModal !== 'function') return;
|
|
648
|
+
mdModel = m;
|
|
649
|
+
Array.prototype.forEach.call(document.querySelectorAll('#modelDialog [data-mdspan]'), function(btn) {
|
|
650
|
+
var on = btn.getAttribute('data-mdspan') === String(mdSpan);
|
|
651
|
+
btn.classList.toggle('on', on);
|
|
652
|
+
btn.onclick = function() {
|
|
653
|
+
mdSpan = btn.getAttribute('data-mdspan') === 'all' ? 'all' : Number(btn.getAttribute('data-mdspan'));
|
|
654
|
+
Array.prototype.forEach.call(document.querySelectorAll('#modelDialog [data-mdspan]'), function(o) { o.classList.toggle('on', o === btn); });
|
|
655
|
+
renderModelDialog(mdModel);
|
|
656
|
+
};
|
|
657
|
+
});
|
|
658
|
+
renderModelDialog(m);
|
|
659
|
+
if (!dlg.open) dlg.showModal();
|
|
660
|
+
}
|
|
381
661
|
function dayModelRows(key) {
|
|
382
662
|
var per = (DATA.byDayModel || {})[key] || {};
|
|
383
663
|
var tops = topModels(DATA.byModel || {}, 8), rows = [], other = 0;
|
|
@@ -537,18 +817,35 @@
|
|
|
537
817
|
});
|
|
538
818
|
}
|
|
539
819
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
820
|
+
var modelPage = 1, modelPer = 10, recentPage = 1, recentPer = 25;
|
|
821
|
+
// Recent requests default to newest-first, which is how the transcript
|
|
822
|
+
// arrives; every column is sortable like the command table.
|
|
823
|
+
var recentSort = { key: 'when', dir: -1 }, recentRows = [];
|
|
824
|
+
function recentVal(r, key) {
|
|
825
|
+
if (key === 'model') return r.m.toLowerCase();
|
|
826
|
+
if (key === 'input') return r.i;
|
|
827
|
+
if (key === 'output') return r.o;
|
|
828
|
+
if (key === 'time') return r.d === undefined ? null : r.d;
|
|
829
|
+
if (key === 'status') return statusRank(r);
|
|
830
|
+
if (key === 'cost') return displayCost(r);
|
|
831
|
+
return r.t;
|
|
832
|
+
}
|
|
833
|
+
function sortRecent() {
|
|
834
|
+
var k = recentSort.key, d = recentSort.dir;
|
|
835
|
+
recentRows.sort(function(a, b) {
|
|
836
|
+
var av = recentVal(a, k), bv = recentVal(b, k);
|
|
837
|
+
if (av === null || av === undefined) return 1;
|
|
838
|
+
if (bv === null || bv === undefined) return -1;
|
|
839
|
+
if (av < bv) return -d;
|
|
840
|
+
if (av > bv) return d;
|
|
841
|
+
return b.t - a.t;
|
|
842
|
+
});
|
|
843
|
+
Array.prototype.forEach.call(document.querySelectorAll('#recentTable .thsort'), function(btn) {
|
|
844
|
+
var on = btn.dataset.sort === k;
|
|
845
|
+
btn.querySelector('.arr').textContent = on ? (d === 1 ? '↑' : '↓') : '⇅';
|
|
846
|
+
btn.closest('th').setAttribute('aria-sort', on ? (d === 1 ? 'ascending' : 'descending') : 'none');
|
|
546
847
|
});
|
|
547
|
-
if (!prev) return cur ? 'new' : '-';
|
|
548
|
-
var p = Math.round((cur - prev) / prev * 100);
|
|
549
|
-
return (p >= 0 ? '+' : '') + p + '%';
|
|
550
848
|
}
|
|
551
|
-
var modelPage = 1, modelPer = 10, recentPage = 1, recentPer = 15;
|
|
552
849
|
function renderModels() {
|
|
553
850
|
var byModel = DATA.byModel || {};
|
|
554
851
|
var tops = topModels(byModel, Object.keys(byModel).length).filter(function(m) { return modelTotal(byModel, m) > 0; });
|
|
@@ -556,7 +853,7 @@
|
|
|
556
853
|
cards.innerHTML = '';
|
|
557
854
|
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>';
|
|
558
855
|
tops.slice(0, 3).forEach(function(m, i) {
|
|
559
|
-
var v = vendorOf(m)
|
|
856
|
+
var v = vendorOf(m);
|
|
560
857
|
var card = document.createElement('div');
|
|
561
858
|
card.className = 'modelcard relative p-4 rounded-xl overflow-hidden';
|
|
562
859
|
if (v.svg) {
|
|
@@ -577,7 +874,8 @@
|
|
|
577
874
|
ghostSparkle.setAttribute('aria-hidden', 'true');
|
|
578
875
|
card.appendChild(ghostSparkle);
|
|
579
876
|
}
|
|
580
|
-
card.style.cssText = 'border: 1px solid var(--line); background: var(--panel)';
|
|
877
|
+
card.style.cssText = 'border: 1px solid var(--line); background: var(--panel); cursor: pointer';
|
|
878
|
+
card.setAttribute('role', 'button');
|
|
581
879
|
var head = document.createElement('p');
|
|
582
880
|
head.className = 'mono text-xs mb-3'; head.style.color = 'var(--dim)';
|
|
583
881
|
head.textContent = '0' + (i + 1);
|
|
@@ -587,20 +885,14 @@
|
|
|
587
885
|
badgeWrap.innerHTML = brandHTML(v, false);
|
|
588
886
|
var badge = badgeWrap.firstChild;
|
|
589
887
|
var mid = document.createElement('div'); mid.className = 'min-w-0 flex-1';
|
|
590
|
-
var nm = document.createElement('p'); nm.className = 'mono text-sm font-bold truncate'; nm.textContent = m;
|
|
888
|
+
var nm = document.createElement('p'); nm.className = 'mono text-sm font-bold truncate'; nm.textContent = displayModel(m); nm.title = m;
|
|
591
889
|
var vn = document.createElement('p'); vn.className = 'mono text-xs truncate'; vn.style.color = 'var(--dim)'; vn.textContent = v.name;
|
|
592
890
|
mid.appendChild(nm); mid.appendChild(vn);
|
|
593
891
|
var tot = document.createElement('p'); tot.className = 'mono font-bold text-lg shrink-0'; tot.textContent = fmtShort(modelTotal(byModel, m));
|
|
594
892
|
row.appendChild(badge); row.appendChild(mid); row.appendChild(tot);
|
|
595
893
|
card.appendChild(head); card.appendChild(row);
|
|
596
|
-
if (d !== 'new') {
|
|
597
|
-
var delta = document.createElement('p');
|
|
598
|
-
delta.className = 'mono text-xs mt-3';
|
|
599
|
-
delta.style.color = d.charAt(0) === '-' ? '#f87171' : 'var(--accent)';
|
|
600
|
-
delta.textContent = d;
|
|
601
|
-
card.appendChild(delta);
|
|
602
|
-
}
|
|
603
894
|
hoverModel(card, m);
|
|
895
|
+
(function(model) { card.addEventListener('click', function() { openModelDialog(model); }); })(m);
|
|
604
896
|
cards.appendChild(card);
|
|
605
897
|
});
|
|
606
898
|
var ol = document.getElementById('models');
|
|
@@ -618,7 +910,7 @@
|
|
|
618
910
|
badgeWrap.innerHTML = brandHTML(v, true);
|
|
619
911
|
var badge = badgeWrap.firstChild;
|
|
620
912
|
var mid = document.createElement('div'); mid.className = 'min-w-0 flex-1';
|
|
621
|
-
var nm = document.createElement('p'); nm.className = 'mono text-sm truncate'; nm.textContent = m;
|
|
913
|
+
var nm = document.createElement('p'); nm.className = 'mono text-sm truncate'; nm.textContent = displayModel(m); nm.title = m;
|
|
622
914
|
var track = document.createElement('div'); track.className = 'bar-track mt-1.5 h-1.5 overflow-hidden';
|
|
623
915
|
var fill = document.createElement('div'); fill.className = 'bar-fill h-full';
|
|
624
916
|
var mv = modelTotal(byModel, m);
|
|
@@ -632,6 +924,7 @@
|
|
|
632
924
|
right.appendChild(n); right.appendChild(cost);
|
|
633
925
|
li.appendChild(badge); li.appendChild(mid); li.appendChild(right);
|
|
634
926
|
hoverModel(li, m);
|
|
927
|
+
(function(model) { li.addEventListener('click', function() { openModelDialog(model); }); })(m);
|
|
635
928
|
ol.appendChild(li);
|
|
636
929
|
});
|
|
637
930
|
document.getElementById('modelsCount').textContent = tops.length ? tops.length + ' models' : '';
|
|
@@ -662,8 +955,12 @@
|
|
|
662
955
|
|
|
663
956
|
// Union of session tool calls + RTK-metered commands, sortable + paged.
|
|
664
957
|
// Session tools were never metered per command: saved / avg / time show – and sort last.
|
|
958
|
+
// Rows group by command alone, machine-wide: same command in any repo is one row.
|
|
665
959
|
var cmdSort = { key: 'count', dir: -1 }, cmdPage = 1, cmdPer = 15, cmdRows = [];
|
|
666
|
-
function cmdVal(r, key) {
|
|
960
|
+
function cmdVal(r, key) {
|
|
961
|
+
if (key === 'name') return r.name.toLowerCase();
|
|
962
|
+
return r[key];
|
|
963
|
+
}
|
|
667
964
|
function sortCmd() {
|
|
668
965
|
var k = cmdSort.key, d = cmdSort.dir;
|
|
669
966
|
cmdRows.sort(function(a, b) {
|
|
@@ -682,12 +979,16 @@
|
|
|
682
979
|
}
|
|
683
980
|
function renderCmd() {
|
|
684
981
|
var g = (DATA.rtkGain || {});
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
982
|
+
var byName = {};
|
|
983
|
+
function add(name, count, saved, avgPct, avgMs) {
|
|
984
|
+
var r = byName[name];
|
|
985
|
+
if (!r) { byName[name] = { name: name, count: count, saved: saved, avgPct: avgPct, avgMs: avgMs }; return; }
|
|
986
|
+
r.count += count;
|
|
987
|
+
if (saved !== null && saved !== undefined) r.saved = (r.saved || 0) + saved;
|
|
988
|
+
}
|
|
989
|
+
(DATA.byTool || []).forEach(function(r) { add(r[0], r[1], null, null, null); });
|
|
990
|
+
(g.byCommand || []).forEach(function(r) { add(r.command, r.count, r.saved, r.avgPct, r.avgMs); });
|
|
991
|
+
cmdRows = Object.keys(byName).map(function(k) { return byName[k]; });
|
|
691
992
|
sortCmd();
|
|
692
993
|
var pages = Math.max(1, Math.ceil(cmdRows.length / cmdPer));
|
|
693
994
|
if (cmdPage > pages) cmdPage = pages;
|
|
@@ -720,9 +1021,8 @@
|
|
|
720
1021
|
});
|
|
721
1022
|
document.getElementById('cmdTable').style.display = cmdRows.length ? '' : 'none';
|
|
722
1023
|
document.getElementById('emptyCmd').classList.toggle('hidden', cmdRows.length > 0);
|
|
723
|
-
var tools = (DATA.byTool || []).length;
|
|
724
1024
|
document.getElementById('toolsScope').textContent =
|
|
725
|
-
g.commands ? fmt(
|
|
1025
|
+
g.commands ? fmt(cmdRows.length) + ' commands · ' + fmt(g.commands) + ' runs · ' + fmtShort(g.saved) + ' saved' : 'tool calls in sessions';
|
|
726
1026
|
paintCmdPager(pages);
|
|
727
1027
|
}
|
|
728
1028
|
// Shared page-number painter: prev/next plus windowed numbers with
|
|
@@ -795,6 +1095,19 @@
|
|
|
795
1095
|
bindPerPage('section[aria-label="All models"]', function() { return modelPer; }, function(n) { modelPer = n; modelPage = 1; }, renderModels);
|
|
796
1096
|
bindPerPage('section[aria-label="Recent requests"]', function() { return recentPer; }, function(n) { recentPer = n; recentPage = 1; }, renderRecent);
|
|
797
1097
|
}
|
|
1098
|
+
function bindRecentTable() {
|
|
1099
|
+
if (bindRecentTable.done) return;
|
|
1100
|
+
bindRecentTable.done = true;
|
|
1101
|
+
Array.prototype.forEach.call(document.querySelectorAll('#recentTable .thsort'), function(btn) {
|
|
1102
|
+
btn.addEventListener('click', function() {
|
|
1103
|
+
var k = btn.dataset.sort;
|
|
1104
|
+
if (recentSort.key === k) recentSort.dir = -recentSort.dir;
|
|
1105
|
+
else { recentSort.key = k; recentSort.dir = k === 'model' || k === 'status' ? 1 : -1; }
|
|
1106
|
+
recentPage = 1;
|
|
1107
|
+
renderRecent();
|
|
1108
|
+
});
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
798
1111
|
function fmtMs(ms) {
|
|
799
1112
|
if (ms < 1000) return Math.round(ms) + 'ms';
|
|
800
1113
|
return (ms / 1000).toFixed(1) + 's';
|
|
@@ -887,6 +1200,22 @@
|
|
|
887
1200
|
|
|
888
1201
|
function render(d) {
|
|
889
1202
|
DATA = d;
|
|
1203
|
+
// Server-provided default currency (tersio gain --currency); a saved
|
|
1204
|
+
// picker choice in localStorage always wins.
|
|
1205
|
+
if (!render.fxInit) {
|
|
1206
|
+
render.fxInit = true;
|
|
1207
|
+
var saved = null;
|
|
1208
|
+
try { saved = localStorage.getItem('tersio-fx-cur'); } catch (e) { }
|
|
1209
|
+
if (!saved && d.currency && CURS[d.currency]) {
|
|
1210
|
+
fx.cur = d.currency;
|
|
1211
|
+
var curEl = document.getElementById('fxCur');
|
|
1212
|
+
if (curEl) curEl.textContent = FLAGS[fx.cur] + ' ' + fx.cur;
|
|
1213
|
+
var panel = document.getElementById('fxPanel');
|
|
1214
|
+
if (panel) Array.prototype.forEach.call(panel.children, function(o) {
|
|
1215
|
+
o.setAttribute('aria-selected', o.dataset.cur === fx.cur ? 'true' : 'false');
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
890
1219
|
var t = d.tokens || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
891
1220
|
var total = t.input + t.output + t.cacheRead + t.cacheWrite;
|
|
892
1221
|
var totalEl = document.getElementById('total');
|
|
@@ -903,7 +1232,7 @@
|
|
|
903
1232
|
renderSpark(byDay, d.usd || 0, d.savedUsd || 0, total);
|
|
904
1233
|
document.getElementById('verChip').textContent = 'tersio v' + (d.version || '?');
|
|
905
1234
|
var dp = d.paths || {};
|
|
906
|
-
[['pathLedger', dp.ledger], ['pathSessions', dp.sessions]].forEach(function(pair) {
|
|
1235
|
+
[['pathLedger', dp.ledger], ['pathSessions', dp.sessions], ['pathUsageDb', dp.usageDb]].forEach(function(pair) {
|
|
907
1236
|
var pel = document.getElementById(pair[0]);
|
|
908
1237
|
if (pel && pair[1]) { pel.textContent = pair[1]; pel.title = pair[1]; }
|
|
909
1238
|
});
|
|
@@ -981,6 +1310,7 @@
|
|
|
981
1310
|
renderStrip(t);
|
|
982
1311
|
bindGraphTabs();
|
|
983
1312
|
bindCmdTable();
|
|
1313
|
+
bindRecentTable();
|
|
984
1314
|
renderGraph(d.byDay || {});
|
|
985
1315
|
renderModels();
|
|
986
1316
|
renderRecent();
|
|
@@ -993,36 +1323,70 @@
|
|
|
993
1323
|
}
|
|
994
1324
|
observe();
|
|
995
1325
|
}
|
|
1326
|
+
// shadcn/ui badge: default/secondary/destructive/outline carry the meaning,
|
|
1327
|
+
// the label carries the detail. Built as DOM nodes so provider error notes
|
|
1328
|
+
// never reach innerHTML.
|
|
1329
|
+
function statusBadge(r) {
|
|
1330
|
+
var b = document.createElement('span');
|
|
1331
|
+
b.className = 'badge';
|
|
1332
|
+
if (r.st === 'error') {
|
|
1333
|
+
b.classList.add('destructive');
|
|
1334
|
+
b.textContent = statusLabel(r);
|
|
1335
|
+
b.title = 'error' + (r.code ? ' ' + r.code : '') + (r.note ? ' — ' + r.note : '');
|
|
1336
|
+
} else if (r.st === 'aborted') {
|
|
1337
|
+
b.classList.add('outline');
|
|
1338
|
+
b.textContent = 'aborted';
|
|
1339
|
+
b.title = r.note || 'Interrupted by user';
|
|
1340
|
+
} else {
|
|
1341
|
+
b.classList.add('secondary');
|
|
1342
|
+
b.textContent = 'completed';
|
|
1343
|
+
if (r.note) b.title = r.note;
|
|
1344
|
+
}
|
|
1345
|
+
return b;
|
|
1346
|
+
}
|
|
996
1347
|
function renderRecent() {
|
|
997
1348
|
var body = document.getElementById('recent');
|
|
998
1349
|
body.innerHTML = '';
|
|
999
1350
|
var all = DATA.recent || [];
|
|
1000
|
-
|
|
1351
|
+
recentRows = all.slice();
|
|
1352
|
+
sortRecent();
|
|
1353
|
+
var recentPages = Math.max(1, Math.ceil(recentRows.length / recentPer));
|
|
1001
1354
|
if (recentPage > recentPages) recentPage = recentPages;
|
|
1002
1355
|
var recentStart = (recentPage - 1) * recentPer;
|
|
1003
|
-
var rows =
|
|
1356
|
+
var rows = recentRows.slice(recentStart, recentStart + recentPer);
|
|
1004
1357
|
rows.forEach(function(r, i) {
|
|
1005
1358
|
var v = vendorOf(r.m);
|
|
1359
|
+
var measured = costIsMeasured(r);
|
|
1006
1360
|
var tr = document.createElement('tr');
|
|
1007
|
-
tr.className = 'rrow' + (i < Math.min(
|
|
1008
|
-
var dot = '<span style="display:inline-block;width:8px;height:8px;border-radius:99px;background:' + v.color + ';margin-right:8px"></span>';
|
|
1361
|
+
tr.className = 'rrow' + (i < Math.min(recentRows.length - recentStart, recentPer) - 1 ? ' rowline' : '');
|
|
1009
1362
|
tr.innerHTML = '<td class="py-2.5 pr-3 truncate" style="min-width: 0"></td>' +
|
|
1010
|
-
'<td class="text-right py-2.5 pr-3 whitespace-nowrap"></td>' +
|
|
1363
|
+
'<td class="text-right py-2.5 pr-3 whitespace-nowrap" style="color:#fb923c"></td>' +
|
|
1364
|
+
'<td class="text-right py-2.5 pr-3 whitespace-nowrap" style="color:var(--accent)"></td>' +
|
|
1365
|
+
'<td class="text-right py-2.5 pr-3 whitespace-nowrap" style="color:var(--dim)">–</td>' +
|
|
1366
|
+
'<td class="py-2.5 pr-3 whitespace-nowrap"></td>' +
|
|
1011
1367
|
'<td class="text-right py-2.5 pr-3 whitespace-nowrap"></td>' +
|
|
1012
1368
|
'<td class="text-right py-2.5 whitespace-nowrap" style="color: var(--dim)"></td>';
|
|
1013
1369
|
var tds = tr.children;
|
|
1370
|
+
var dot = '<span style="display:inline-block;width:8px;height:8px;border-radius:99px;background:' + v.color + ';margin-right:8px"></span>';
|
|
1014
1371
|
tds[0].innerHTML = dot + '<span></span>';
|
|
1015
|
-
tds[0].querySelector('span:last-child').textContent = r.m;
|
|
1016
|
-
tds[1].
|
|
1017
|
-
tds[
|
|
1018
|
-
tds[
|
|
1019
|
-
tds[
|
|
1020
|
-
tds[
|
|
1372
|
+
tds[0].querySelector('span:last-child').textContent = displayModel(r.m);
|
|
1373
|
+
tds[1].textContent = fmt(r.i);
|
|
1374
|
+
tds[2].textContent = fmt(r.o);
|
|
1375
|
+
tds[3].textContent = speedText(r);
|
|
1376
|
+
tds[4].appendChild(statusBadge(r));
|
|
1377
|
+
tds[5].textContent = (measured ? '' : '~') + fxMoney(displayCost(r));
|
|
1378
|
+
tds[5].title = measured
|
|
1379
|
+
? 'measured — charged by the provider'
|
|
1380
|
+
: (costRecorded(r)
|
|
1381
|
+
? 'est. — provider charged ' + fxMoney(r.usd) + ' (free or local), modeled from tokens'
|
|
1382
|
+
: 'est. — modeled from tokens');
|
|
1383
|
+
if (measured) tds[5].style.color = 'var(--ink)';
|
|
1384
|
+
tds[6].textContent = whenStamp(r.t);
|
|
1021
1385
|
hoverRecent(tr, r);
|
|
1022
1386
|
body.appendChild(tr);
|
|
1023
1387
|
});
|
|
1024
|
-
document.getElementById('recentCount').textContent = all.length ?
|
|
1025
|
-
paintPager('recentPages', 'recentRange', recentPage, recentPer,
|
|
1388
|
+
document.getElementById('recentCount').textContent = all.length ? recentRows.length + ' requests' : '';
|
|
1389
|
+
paintPager('recentPages', 'recentRange', recentPage, recentPer, recentRows.length, function(p) { recentPage = p; renderRecent(); });
|
|
1026
1390
|
document.getElementById('recentTable').style.display = all.length ? '' : 'none';
|
|
1027
1391
|
document.getElementById('emptyRecent').classList.toggle('hidden', all.length > 0);
|
|
1028
1392
|
}
|
|
@@ -1091,12 +1455,16 @@
|
|
|
1091
1455
|
document.getElementById('reload').addEventListener('click', load);
|
|
1092
1456
|
(function() {
|
|
1093
1457
|
var dlg = document.getElementById('settings');
|
|
1094
|
-
if (
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1458
|
+
if (dlg && typeof dlg.showModal === 'function') {
|
|
1459
|
+
document.getElementById('settingsBtn').addEventListener('click', function() { dlg.showModal(); });
|
|
1460
|
+
document.getElementById('settingsClose').addEventListener('click', function() { dlg.close(); });
|
|
1461
|
+
dlg.addEventListener('click', function(ev) { if (ev.target === dlg) dlg.close(); });
|
|
1462
|
+
}
|
|
1463
|
+
var md = document.getElementById('modelDialog');
|
|
1464
|
+
if (md && typeof md.showModal === 'function') {
|
|
1465
|
+
document.getElementById('mdClose').addEventListener('click', function() { md.close(); });
|
|
1466
|
+
md.addEventListener('click', function(ev) { if (ev.target === md) md.close(); });
|
|
1467
|
+
}
|
|
1100
1468
|
})();
|
|
1101
1469
|
(function() {
|
|
1102
1470
|
var resetBtn = document.getElementById('reset');
|