@krtclcdy/tersio 2.18.0 → 2.20.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 +2 -2
- package/cli/common.js +21 -3
- package/cli/currency.js +102 -0
- package/cli/dashboard.js +54 -2
- package/cli/doctor.js +9 -7
- package/cli/install.js +35 -101
- package/cli/profile.js +92 -0
- package/cli/reset.js +4 -2
- package/cli/settings.js +122 -0
- package/cli/usage.js +38 -24
- package/dashboard/app.js +297 -70
- package/dashboard/styles.css +58 -1
- package/dashboard/template.html +75 -3
- package/extensions/ai-addons-updater/index.ts +377 -0
- package/extensions/caveman-session/index.ts +161 -0
- package/extensions/combo-toggle/index.ts +217 -0
- package/extensions/lib/utils.ts +162 -0
- package/extensions/rtk-session/index.ts +164 -0
- package/extensions/shared/carbon.ts +123 -0
- package/extensions/shared/mode-reinforcement.ts +29 -0
- package/extensions/shared/plugin-settings.ts +57 -0
- package/extensions/shared/pricing.ts +168 -0
- package/extensions/shared/rtk-gain.ts +105 -0
- package/extensions/shared/session-state.ts +201 -0
- package/extensions/shared/types.ts +85 -0
- package/extensions/shared/usage-ledger.ts +406 -0
- package/extensions/shared/usage-store.ts +325 -0
- package/extensions/tersio-commands/index.ts +110 -0
- package/package.json +36 -24
- package/tersio.js +8 -1
- package/extensions/ai-addons-updater/index.js +0 -331
- package/extensions/caveman-session/index.js +0 -152
- package/extensions/combo-toggle/index.js +0 -165
- package/extensions/lib/utils.js +0 -154
- package/extensions/rtk-session/index.js +0 -148
- package/extensions/shared/carbon.js +0 -88
- package/extensions/shared/mode-reinforcement.js +0 -26
- package/extensions/shared/plugin-settings.js +0 -51
- package/extensions/shared/pricing.js +0 -163
- package/extensions/shared/rtk-gain.js +0 -82
- package/extensions/shared/session-state.js +0 -177
- package/extensions/shared/types.js +0 -3
- package/extensions/shared/usage-ledger.js +0 -333
- package/extensions/tersio-commands/index.js +0 -102
package/dashboard/app.js
CHANGED
|
@@ -116,6 +116,14 @@
|
|
|
116
116
|
function setCur(k) {
|
|
117
117
|
fx.cur = k; active = keys.indexOf(k);
|
|
118
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
|
+
}
|
|
119
127
|
paint();
|
|
120
128
|
if (DATA) render(DATA);
|
|
121
129
|
}
|
|
@@ -250,13 +258,29 @@
|
|
|
250
258
|
var b = byModel[m] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
251
259
|
return b.input + b.output + b.cacheRead + b.cacheWrite;
|
|
252
260
|
}
|
|
253
|
-
|
|
254
|
-
var DATA = null, RANGE = 'daily', cardsShown = false;
|
|
255
|
-
|
|
261
|
+
var DATA = null, cardsShown = false;
|
|
256
262
|
function topModels(byModel, n) {
|
|
257
263
|
return Object.keys(byModel).sort(function(a, b) { return modelTotal(byModel, b) - modelTotal(byModel, a); }).slice(0, n);
|
|
258
264
|
}
|
|
259
|
-
|
|
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
|
+
}
|
|
260
284
|
function allDays() {
|
|
261
285
|
var set = {};
|
|
262
286
|
Object.keys(DATA.byDay || {}).forEach(function(d) { set[d] = 1; });
|
|
@@ -293,7 +317,7 @@
|
|
|
293
317
|
var dt = new Date(key + 'T12:00:00');
|
|
294
318
|
return isNaN(dt) ? key : dt.toLocaleString('en-US', { month: 'short', day: 'numeric' }).toUpperCase();
|
|
295
319
|
}
|
|
296
|
-
function shortName(m) { return
|
|
320
|
+
function shortName(m) { var d = displayModel(m); return d.length > 22 ? d.slice(0, 21) + '...' : d; }
|
|
297
321
|
function tipDayHTML(key, total, rows) {
|
|
298
322
|
var h = '<div class="tt">' + dateHead(key) + '</div><div class="tv">' + fmtShort(total) + ' total</div>';
|
|
299
323
|
rows.forEach(function(r) {
|
|
@@ -418,6 +442,222 @@
|
|
|
418
442
|
el.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
|
|
419
443
|
el.addEventListener('mouseleave', hideTip);
|
|
420
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
|
+
}
|
|
421
661
|
function dayModelRows(key) {
|
|
422
662
|
var per = (DATA.byDayModel || {})[key] || {};
|
|
423
663
|
var tops = topModels(DATA.byModel || {}, 8), rows = [], other = 0;
|
|
@@ -634,7 +874,8 @@
|
|
|
634
874
|
ghostSparkle.setAttribute('aria-hidden', 'true');
|
|
635
875
|
card.appendChild(ghostSparkle);
|
|
636
876
|
}
|
|
637
|
-
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');
|
|
638
879
|
var head = document.createElement('p');
|
|
639
880
|
head.className = 'mono text-xs mb-3'; head.style.color = 'var(--dim)';
|
|
640
881
|
head.textContent = '0' + (i + 1);
|
|
@@ -644,13 +885,14 @@
|
|
|
644
885
|
badgeWrap.innerHTML = brandHTML(v, false);
|
|
645
886
|
var badge = badgeWrap.firstChild;
|
|
646
887
|
var mid = document.createElement('div'); mid.className = 'min-w-0 flex-1';
|
|
647
|
-
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;
|
|
648
889
|
var vn = document.createElement('p'); vn.className = 'mono text-xs truncate'; vn.style.color = 'var(--dim)'; vn.textContent = v.name;
|
|
649
890
|
mid.appendChild(nm); mid.appendChild(vn);
|
|
650
891
|
var tot = document.createElement('p'); tot.className = 'mono font-bold text-lg shrink-0'; tot.textContent = fmtShort(modelTotal(byModel, m));
|
|
651
892
|
row.appendChild(badge); row.appendChild(mid); row.appendChild(tot);
|
|
652
893
|
card.appendChild(head); card.appendChild(row);
|
|
653
894
|
hoverModel(card, m);
|
|
895
|
+
(function(model) { card.addEventListener('click', function() { openModelDialog(model); }); })(m);
|
|
654
896
|
cards.appendChild(card);
|
|
655
897
|
});
|
|
656
898
|
var ol = document.getElementById('models');
|
|
@@ -668,7 +910,7 @@
|
|
|
668
910
|
badgeWrap.innerHTML = brandHTML(v, true);
|
|
669
911
|
var badge = badgeWrap.firstChild;
|
|
670
912
|
var mid = document.createElement('div'); mid.className = 'min-w-0 flex-1';
|
|
671
|
-
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;
|
|
672
914
|
var track = document.createElement('div'); track.className = 'bar-track mt-1.5 h-1.5 overflow-hidden';
|
|
673
915
|
var fill = document.createElement('div'); fill.className = 'bar-fill h-full';
|
|
674
916
|
var mv = modelTotal(byModel, m);
|
|
@@ -682,6 +924,7 @@
|
|
|
682
924
|
right.appendChild(n); right.appendChild(cost);
|
|
683
925
|
li.appendChild(badge); li.appendChild(mid); li.appendChild(right);
|
|
684
926
|
hoverModel(li, m);
|
|
927
|
+
(function(model) { li.addEventListener('click', function() { openModelDialog(model); }); })(m);
|
|
685
928
|
ol.appendChild(li);
|
|
686
929
|
});
|
|
687
930
|
document.getElementById('modelsCount').textContent = tops.length ? tops.length + ' models' : '';
|
|
@@ -712,43 +955,12 @@
|
|
|
712
955
|
|
|
713
956
|
// Union of session tool calls + RTK-metered commands, sortable + paged.
|
|
714
957
|
// Session tools were never metered per command: saved / avg / time show – and sort last.
|
|
715
|
-
//
|
|
716
|
-
|
|
717
|
-
var cmdSort = { key: 'count', dir: -1 }, cmdPage = 1, cmdPer = 15, cmdRows = [], cmdProject = '';
|
|
958
|
+
// Rows group by command alone, machine-wide: same command in any repo is one row.
|
|
959
|
+
var cmdSort = { key: 'count', dir: -1 }, cmdPage = 1, cmdPer = 15, cmdRows = [];
|
|
718
960
|
function cmdVal(r, key) {
|
|
719
961
|
if (key === 'name') return r.name.toLowerCase();
|
|
720
|
-
if (key === 'project') return (r.project || '').toLowerCase();
|
|
721
962
|
return r[key];
|
|
722
963
|
}
|
|
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
|
-
}
|
|
752
964
|
function sortCmd() {
|
|
753
965
|
var k = cmdSort.key, d = cmdSort.dir;
|
|
754
966
|
cmdRows.sort(function(a, b) {
|
|
@@ -767,14 +979,16 @@
|
|
|
767
979
|
}
|
|
768
980
|
function renderCmd() {
|
|
769
981
|
var g = (DATA.rtkGain || {});
|
|
770
|
-
var
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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]; });
|
|
778
992
|
sortCmd();
|
|
779
993
|
var pages = Math.max(1, Math.ceil(cmdRows.length / cmdPer));
|
|
780
994
|
if (cmdPage > pages) cmdPage = pages;
|
|
@@ -789,7 +1003,6 @@
|
|
|
789
1003
|
tr.className = 'mrow' + (i < Math.min(cmdRows.length - start, cmdPer) - 1 ? ' rowline' : '');
|
|
790
1004
|
tr.innerHTML = '<td class="text-right pr-3 py-2.5 mono text-xs w-10" style="color: var(--dim)"></td>' +
|
|
791
1005
|
'<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>' +
|
|
793
1006
|
'<td class="text-right py-2.5 pr-3 font-bold"></td>' +
|
|
794
1007
|
'<td class="text-right py-2.5 pr-3 font-bold"></td>' +
|
|
795
1008
|
'<td class="text-right py-2.5 pr-3" style="color: var(--accent)"></td>' +
|
|
@@ -799,20 +1012,17 @@
|
|
|
799
1012
|
tds[0].textContent = String(n + 1).padStart(2, '0');
|
|
800
1013
|
tds[1].textContent = r.name;
|
|
801
1014
|
tds[1].title = r.name;
|
|
802
|
-
tds[2].textContent =
|
|
803
|
-
tds[
|
|
804
|
-
tds[
|
|
805
|
-
tds[
|
|
806
|
-
tds[
|
|
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';
|
|
1015
|
+
tds[2].textContent = fmt(r.count);
|
|
1016
|
+
tds[3].textContent = r.saved === null ? '–' : fmtShort(r.saved);
|
|
1017
|
+
tds[4].textContent = r.avgPct === null ? '–' : r.avgPct.toFixed(1) + '%';
|
|
1018
|
+
tds[5].textContent = r.avgMs === null || r.avgMs === undefined ? '–' : fmtMs(r.avgMs);
|
|
1019
|
+
tds[6].firstChild.firstChild.style.width = r.count ? Math.round(r.count / max * 100) + '%' : '0';
|
|
809
1020
|
body.appendChild(tr);
|
|
810
1021
|
});
|
|
811
1022
|
document.getElementById('cmdTable').style.display = cmdRows.length ? '' : 'none';
|
|
812
1023
|
document.getElementById('emptyCmd').classList.toggle('hidden', cmdRows.length > 0);
|
|
813
|
-
var tools = (DATA.byTool || []).length;
|
|
814
1024
|
document.getElementById('toolsScope').textContent =
|
|
815
|
-
g.commands ? fmt(
|
|
1025
|
+
g.commands ? fmt(cmdRows.length) + ' commands · ' + fmt(g.commands) + ' runs · ' + fmtShort(g.saved) + ' saved' : 'tool calls in sessions';
|
|
816
1026
|
paintCmdPager(pages);
|
|
817
1027
|
}
|
|
818
1028
|
// Shared page-number painter: prev/next plus windowed numbers with
|
|
@@ -882,8 +1092,6 @@
|
|
|
882
1092
|
});
|
|
883
1093
|
});
|
|
884
1094
|
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
1095
|
bindPerPage('section[aria-label="All models"]', function() { return modelPer; }, function(n) { modelPer = n; modelPage = 1; }, renderModels);
|
|
888
1096
|
bindPerPage('section[aria-label="Recent requests"]', function() { return recentPer; }, function(n) { recentPer = n; recentPage = 1; }, renderRecent);
|
|
889
1097
|
}
|
|
@@ -992,6 +1200,22 @@
|
|
|
992
1200
|
|
|
993
1201
|
function render(d) {
|
|
994
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
|
+
}
|
|
995
1219
|
var t = d.tokens || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
996
1220
|
var total = t.input + t.output + t.cacheRead + t.cacheWrite;
|
|
997
1221
|
var totalEl = document.getElementById('total');
|
|
@@ -1008,7 +1232,7 @@
|
|
|
1008
1232
|
renderSpark(byDay, d.usd || 0, d.savedUsd || 0, total);
|
|
1009
1233
|
document.getElementById('verChip').textContent = 'tersio v' + (d.version || '?');
|
|
1010
1234
|
var dp = d.paths || {};
|
|
1011
|
-
[['pathLedger', dp.ledger], ['pathSessions', dp.sessions]].forEach(function(pair) {
|
|
1235
|
+
[['pathLedger', dp.ledger], ['pathSessions', dp.sessions], ['pathUsageDb', dp.usageDb]].forEach(function(pair) {
|
|
1012
1236
|
var pel = document.getElementById(pair[0]);
|
|
1013
1237
|
if (pel && pair[1]) { pel.textContent = pair[1]; pel.title = pair[1]; }
|
|
1014
1238
|
});
|
|
@@ -1145,7 +1369,7 @@
|
|
|
1145
1369
|
var tds = tr.children;
|
|
1146
1370
|
var dot = '<span style="display:inline-block;width:8px;height:8px;border-radius:99px;background:' + v.color + ';margin-right:8px"></span>';
|
|
1147
1371
|
tds[0].innerHTML = dot + '<span></span>';
|
|
1148
|
-
tds[0].querySelector('span:last-child').textContent = r.m;
|
|
1372
|
+
tds[0].querySelector('span:last-child').textContent = displayModel(r.m);
|
|
1149
1373
|
tds[1].textContent = fmt(r.i);
|
|
1150
1374
|
tds[2].textContent = fmt(r.o);
|
|
1151
1375
|
tds[3].textContent = speedText(r);
|
|
@@ -1158,7 +1382,6 @@
|
|
|
1158
1382
|
: 'est. — modeled from tokens');
|
|
1159
1383
|
if (measured) tds[5].style.color = 'var(--ink)';
|
|
1160
1384
|
tds[6].textContent = whenStamp(r.t);
|
|
1161
|
-
tds[6].title = stampLocal(r.t);
|
|
1162
1385
|
hoverRecent(tr, r);
|
|
1163
1386
|
body.appendChild(tr);
|
|
1164
1387
|
});
|
|
@@ -1232,12 +1455,16 @@
|
|
|
1232
1455
|
document.getElementById('reload').addEventListener('click', load);
|
|
1233
1456
|
(function() {
|
|
1234
1457
|
var dlg = document.getElementById('settings');
|
|
1235
|
-
if (
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
+
}
|
|
1241
1468
|
})();
|
|
1242
1469
|
(function() {
|
|
1243
1470
|
var resetBtn = document.getElementById('reset');
|
package/dashboard/styles.css
CHANGED
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
.mrow:hover { background: var(--track); box-shadow: var(--shadow-xs); }
|
|
127
127
|
.stripcell { transition: background .25s; }
|
|
128
128
|
.stripcell:hover { box-shadow: var(--shadow-xs); }
|
|
129
|
-
button, .tilt, .cell, .stripcell { cursor: pointer; }
|
|
129
|
+
button, .tilt, .cell, .stripcell, .modelcard, .mrow { cursor: pointer; }
|
|
130
130
|
.btn-push { transition: transform .12s, background .2s; }
|
|
131
131
|
.btn-push:hover { background: var(--accent-soft); }
|
|
132
132
|
.btn-push:active { transform: scale(.96); }
|
|
@@ -245,3 +245,60 @@ html[data-theme="dark"] .brandmark.placeholder { background: #fff !important; co
|
|
|
245
245
|
.toast-desc { font-size: 12px; color: var(--dim); margin: 1px 0 0; }
|
|
246
246
|
@keyframes toast-in { from { opacity: 0; transform: translateY(8px) scale(.98); } }
|
|
247
247
|
@keyframes toast-out { to { opacity: 0; transform: translateY(6px) scale(.98); } }
|
|
248
|
+
/* Model detail dialog (shadcn chart language, native modal). KPI strip,
|
|
249
|
+
gradient area chart with range tabs, rounded bars, token-mix donut. */
|
|
250
|
+
.dlg.mdlg { width: min(880px, calc(100vw - 32px)); max-height: min(88vh, 960px); padding: 0; overflow: hidden; display: none; flex-direction: column; }
|
|
251
|
+
.dlg.mdlg[open] { display: flex; }
|
|
252
|
+
.dlg.mdlg .dlg-head { padding: 20px 20px 0; }
|
|
253
|
+
.md-body { overflow-y: auto; padding: 0 20px 20px; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--line) transparent; display: grid; gap: 12px; margin-top: 16px; }
|
|
254
|
+
.md-rankpill { border: 1px solid var(--line); background: var(--accent-soft); color: var(--accent); border-radius: 999px; padding: 4px 12px; font-size: 12px; font-weight: 700; white-space: nowrap; }
|
|
255
|
+
.md-kpis { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-flow: dense; gap: 8px; }
|
|
256
|
+
@media (min-width: 640px) { .md-kpis { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
|
|
257
|
+
.md-kpi { border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; background: var(--panel); }
|
|
258
|
+
.md-kpi p { margin: 0; }
|
|
259
|
+
.md-kpi .k { font-size: 10px; text-transform: uppercase; letter-spacing: .14em; color: var(--dim); }
|
|
260
|
+
.md-kpi .v { font-size: 22px; font-weight: 800; letter-spacing: -0.02em; margin-top: 4px; font-variant-numeric: tabular-nums; }
|
|
261
|
+
.md-kpi .d { font-size: 11px; margin-top: 2px; font-variant-numeric: tabular-nums; }
|
|
262
|
+
.md-kpi .d.up { color: var(--accent); }
|
|
263
|
+
.md-kpi .d.flat { color: var(--dim); }
|
|
264
|
+
.md-chart { position: relative; border: 1px solid var(--line); border-radius: 12px; padding: 14px; background: var(--panel); }
|
|
265
|
+
.md-chart-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
|
|
266
|
+
.md-chart-title { font-size: 13px; font-weight: 700; letter-spacing: -0.01em; margin: 0; }
|
|
267
|
+
.md-chart-sub { font-size: 11px; color: var(--dim); margin: 2px 0 0; text-transform: uppercase; letter-spacing: .12em; }
|
|
268
|
+
.md-trend { font-size: 11px; font-weight: 700; white-space: nowrap; border-radius: 999px; padding: 3px 10px; background: var(--track); color: var(--dim); }
|
|
269
|
+
.md-trend.up { background: var(--accent-soft); color: var(--accent); }
|
|
270
|
+
#mdSpark { width: 100%; height: 190px; display: block; }
|
|
271
|
+
#mdSpark .grid { stroke: var(--line); stroke-width: 1; }
|
|
272
|
+
#mdSpark .area { fill: url(#mdGrad); stroke: none; }
|
|
273
|
+
#mdSpark .line { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
|
|
274
|
+
#mdSpark .dot { fill: var(--accent); stroke: var(--panel); stroke-width: 2; }
|
|
275
|
+
#mdSpark .hoverdot { fill: var(--accent); stroke: var(--panel); stroke-width: 2; }
|
|
276
|
+
.md-tip { position: absolute; pointer-events: none; background: var(--panel); color: var(--ink); border: 1px solid var(--line); box-shadow: 0 8px 24px rgba(0, 0, 0, .28); border-radius: 8px; padding: 6px 10px; font-size: 11px; line-height: 1.5; white-space: nowrap; max-width: calc(100% - 28px); overflow: hidden; text-overflow: ellipsis; transform: translate(-50%, -110%); display: none; z-index: 5; font-variant-numeric: tabular-nums; }
|
|
277
|
+
.md-cols { display: grid; grid-template-columns: minmax(0, 1fr); grid-auto-flow: dense; gap: 12px; }
|
|
278
|
+
@media (min-width: 720px) { .md-cols { grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); } }
|
|
279
|
+
.md-months { display: flex; justify-content: space-between; margin-top: 6px; font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: .12em; }
|
|
280
|
+
.md-bars { display: flex; align-items: flex-end; gap: 3px; height: 132px; background-image: linear-gradient(var(--line) 1px, transparent 1px); background-size: 100% 33px; background-position: 0 bottom; }
|
|
281
|
+
.md-bars > span { flex: 1; min-width: 3px; border-radius: 4px 4px 2px 2px; background: var(--accent); opacity: .4; cursor: default; }
|
|
282
|
+
.md-bars > span:hover { opacity: 1; }
|
|
283
|
+
.md-bars > span.top { opacity: 1; box-shadow: 0 0 8px var(--accent-soft); }
|
|
284
|
+
.md-bars > span.zero { background: var(--track); opacity: 1; box-shadow: none; }
|
|
285
|
+
.md-note { font-size: 11px; color: var(--dim); margin: 8px 0 0; font-variant-numeric: tabular-nums; }
|
|
286
|
+
.md-donut-wrap { position: relative; width: 150px; height: 150px; margin: 4px auto 8px; }
|
|
287
|
+
#mdMix { width: 150px; height: 150px; display: block; transform: rotate(-90deg); }
|
|
288
|
+
#mdMix circle { fill: none; stroke-width: 16; }
|
|
289
|
+
#mdMix .tk { stroke: var(--track); }
|
|
290
|
+
.md-donut-center { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; }
|
|
291
|
+
.md-donut-center p { margin: 0; font-size: 17px; font-weight: 800; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
|
|
292
|
+
.md-legend { display: grid; gap: 6px; font-size: 11px; }
|
|
293
|
+
.md-legend .row { display: flex; align-items: center; gap: 8px; }
|
|
294
|
+
.md-legend .dot { width: 8px; height: 8px; border-radius: 999px; flex-shrink: 0; }
|
|
295
|
+
.md-legend .pct { margin-left: auto; color: var(--dim); font-variant-numeric: tabular-nums; }
|
|
296
|
+
.md-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-flow: dense; gap: 8px; }
|
|
297
|
+
@media (min-width: 640px) { .md-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
|
|
298
|
+
.md-stat { border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; background: var(--panel); }
|
|
299
|
+
.md-stat p { margin: 0; }
|
|
300
|
+
.md-stat .k { font-size: 10px; text-transform: uppercase; letter-spacing: .14em; color: var(--dim); }
|
|
301
|
+
.md-stat .v { font-size: 20px; font-weight: 800; letter-spacing: -0.02em; margin-top: 4px; font-variant-numeric: tabular-nums; }
|
|
302
|
+
.md-stat .s { font-size: 11px; color: var(--dim); margin-top: 2px; }
|
|
303
|
+
#mdBadge .brandmark { width: 44px; height: 44px; border-radius: 12px; }
|
|
304
|
+
@media (prefers-reduced-motion: reduce) { .md-bars > span { transition: none; } }
|