pi-mega-compact 0.8.21 โ†’ 0.8.23

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.
Files changed (84) hide show
  1. package/LICENSE +6 -2
  2. package/README.md +1 -1
  3. package/dist/extensions/dashboard-server/dashboard-client-core.js +201 -0
  4. package/dist/extensions/dashboard-server/dashboard-client-game.js +241 -0
  5. package/dist/extensions/dashboard-server/dashboard-client-repos.js +212 -0
  6. package/dist/extensions/dashboard-server/dashboard-client.js +19 -0
  7. package/dist/extensions/dashboard-server/html.js +2 -621
  8. package/dist/extensions/dashboard-server/routes-core.js +62 -0
  9. package/dist/extensions/dashboard-server/routes-game.js +323 -0
  10. package/dist/extensions/dashboard-server/routes-repo.js +170 -0
  11. package/dist/extensions/dashboard-server/routes-sessions.js +159 -0
  12. package/dist/extensions/dashboard-server/routes.js +10 -0
  13. package/dist/extensions/dashboard-server/server.js +26 -623
  14. package/dist/extensions/mega-commands.js +4 -3
  15. package/dist/extensions/mega-events/agent-handlers.js +2 -1
  16. package/dist/extensions/mega-events/compact-handlers.js +26 -0
  17. package/dist/extensions/mega-events/session-handlers.js +2 -1
  18. package/dist/extensions/mega-pipeline/compact.js +3 -2
  19. package/dist/extensions/mega-runtime/state.js +7 -7
  20. package/dist/src/dedup/raptor/multilevel.js +172 -0
  21. package/dist/src/dedup/raptor/multilevel.test.js +203 -0
  22. package/dist/src/dedup/raptor/promote.test.js +5 -5
  23. package/dist/src/dedup/raptor/retrieval.js +1 -1
  24. package/dist/src/dedup/sprint12.test.js +7 -7
  25. package/dist/src/dedup-engine.test.js +29 -29
  26. package/dist/src/e2e.test.js +38 -38
  27. package/dist/src/engine.js +3 -3
  28. package/dist/src/engine.test.js +6 -6
  29. package/dist/src/importance.js +197 -0
  30. package/dist/src/importance.test.js +372 -0
  31. package/dist/src/ratio.bench.test.js +18 -18
  32. package/dist/src/recall.js +6 -5
  33. package/dist/src/recall.test.js +85 -27
  34. package/dist/src/sprint14.test.js +2 -2
  35. package/dist/src/store/migrate.test.js +5 -5
  36. package/dist/src/store/sprint10.test.js +5 -5
  37. package/dist/src/store/sqlite/global-index.js +5 -174
  38. package/dist/src/store/sqlite/global-sessions.js +190 -0
  39. package/dist/src/vector-read.js +168 -0
  40. package/dist/src/vector-search.js +191 -0
  41. package/dist/src/vectorStore.js +10 -297
  42. package/dist/src/vectorStore.test.js +32 -32
  43. package/extensions/dashboard-server/dashboard-client-core.ts +202 -0
  44. package/extensions/dashboard-server/dashboard-client-game.ts +242 -0
  45. package/extensions/dashboard-server/dashboard-client-repos.ts +213 -0
  46. package/extensions/dashboard-server/dashboard-client.ts +21 -0
  47. package/extensions/dashboard-server/html.ts +2 -621
  48. package/extensions/dashboard-server/routes-core.ts +113 -0
  49. package/extensions/dashboard-server/routes-game.ts +386 -0
  50. package/extensions/dashboard-server/routes-repo.ts +212 -0
  51. package/extensions/dashboard-server/routes-sessions.ts +195 -0
  52. package/extensions/dashboard-server/routes.ts +13 -0
  53. package/extensions/dashboard-server/server.ts +37 -700
  54. package/extensions/mega-commands.ts +4 -3
  55. package/extensions/mega-events/agent-handlers.ts +2 -1
  56. package/extensions/mega-events/compact-handlers.ts +28 -0
  57. package/extensions/mega-events/session-handlers.ts +2 -1
  58. package/extensions/mega-pipeline/compact.ts +3 -2
  59. package/extensions/mega-runtime/state.ts +7 -7
  60. package/extensions/openclaw-mega-compact.ts +2 -2
  61. package/package.json +2 -2
  62. package/src/dedup/raptor/multilevel.test.ts +278 -0
  63. package/src/dedup/raptor/multilevel.ts +246 -0
  64. package/src/dedup/raptor/promote.test.ts +5 -5
  65. package/src/dedup/raptor/retrieval.ts +1 -1
  66. package/src/dedup/sprint12.test.ts +7 -7
  67. package/src/dedup-engine.test.ts +30 -30
  68. package/src/e2e.test.ts +38 -38
  69. package/src/engine.test.ts +6 -6
  70. package/src/engine.ts +3 -3
  71. package/src/importance.test.ts +538 -0
  72. package/src/importance.ts +312 -0
  73. package/src/ratio.bench.test.ts +18 -18
  74. package/src/recall.test.ts +101 -29
  75. package/src/recall.ts +9 -9
  76. package/src/sprint14.test.ts +2 -2
  77. package/src/store/migrate.test.ts +5 -5
  78. package/src/store/sprint10.test.ts +5 -5
  79. package/src/store/sqlite/global-index.ts +18 -290
  80. package/src/store/sqlite/global-sessions.ts +291 -0
  81. package/src/vector-read.ts +237 -0
  82. package/src/vector-search.ts +231 -0
  83. package/src/vectorStore.test.ts +32 -32
  84. package/src/vectorStore.ts +29 -356
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { dashboardServerVersion } from "./state.js";
5
5
  import { THEMES, DEFAULT_THEME, themeDataBlock } from "../../src/config/themes.js";
6
+ import { dashboardClientJs } from "./dashboard-client.js";
6
7
  // Server-side injection of every theme's :root[data-theme="<id>"] CSS-var
7
8
  // override block so the client can switch themes instantly by setting
8
9
  // document.documentElement.dataset.theme. PREVENT-PI-004: pure local, no network.
@@ -454,627 +455,7 @@ export function dashboardHtml(tierName) {
454
455
  </div>
455
456
 
456
457
  <script>
457
- (function() {
458
- var evBox = document.getElementById('events');
459
- var evBuffer = [];
460
- var MAX_EV = 50;
461
- var offlineBanner = document.getElementById('offline-banner');
462
-
463
- function bullet(el, on, na) {
464
- el.className = 'bullet ' + (na ? 'bullet-na' : on ? 'bullet-on' : 'bullet-off');
465
- }
466
-
467
- function renderSnapshot(d) {
468
- if (!d || !d.updatedAt) { offlineBanner.style.display = 'block'; return; }
469
- offlineBanner.style.display = 'none';
470
-
471
- var pct = d.context.percent || 0;
472
- document.getElementById('ctx-pct').textContent = pct + '%';
473
- var bar = document.getElementById('ctx-bar');
474
- bar.style.width = Math.max(pct, 1) + '%';
475
- bar.className = 'meter-fill ' + (pct >= 90 ? 'meter-red' : pct >= 70 ? 'meter-yellow' : 'meter-green');
476
- var tok = d.context.tokens != null ? d.context.tokens.toLocaleString() : '?';
477
- var win = d.context.contextWindow ? d.context.contextWindow.toLocaleString() : '?';
478
- document.getElementById('ctx-sub').textContent = tok + ' / ' + win + ' tokens';
479
-
480
- bullet(document.getElementById('tr-armed'), d.trigger.armed, false);
481
- bullet(document.getElementById('tr-ready'), d.trigger.ready, !d.trigger.armed);
482
- var state = d.trigger.ready ? 'THRESHOLD EXCEEDED โ€” compacting next event' :
483
- d.trigger.armed ? 'past fast gate โ€” monitoring token count' : 'idle โ€” below fast gate';
484
- document.getElementById('tr-state').textContent = state;
485
-
486
- // ---- Vector Store โ€” reconciled token accounting (same formula as widget) -
487
- document.getElementById('st-count').textContent = d.store.checkpointCount;
488
- // Compression block from the snapshot (Freed = In โˆ’ Out, single formula).
489
- var c = d.compression || {};
490
- var sess = c.session || { tokensIn:0, tokensOut:0, tokensFreed:0, compressionPct:0, dedupPct:0 };
491
- var cRepo = c.repo || { tokensIn:0, tokensOut:0, tokensFreed:0, compressionPct:0, dedupPct:0 };
492
- document.getElementById('st-in').textContent = sess.tokensIn.toLocaleString();
493
- document.getElementById('st-kept').textContent = sess.tokensOut.toLocaleString();
494
- document.getElementById('st-freed').textContent = sess.tokensFreed.toLocaleString();
495
- var sp = sess.compressionPct || 0;
496
- document.getElementById('st-compress-bar').style.width = Math.max(sp * 100, 0.5) + '%';
497
- document.getElementById('st-compress-bar').className = 'meter-fill ' + (sp >= 0.9 ? 'meter-green' : sp >= 0.6 ? 'meter-yellow' : 'meter-red');
498
- document.getElementById('st-compress-sub').textContent = (sp * 100 >= 10 ? Math.round(sp * 100) : (sp * 100).toFixed(1)) + '% tokens saved ยท dedup: ' + (sess.dedupPct * 100 >= 10 ? Math.round(sess.dedupPct * 100) : (sess.dedupPct * 100).toFixed(1)) + '%';
499
- // ------
500
- document.getElementById('st-injected').textContent = d.store.injectedCount;
501
- document.getElementById('st-dedup').textContent = Math.round(d.store.dedupHitRate * 100) + '%';
502
- var sdr = d.store.storageDedupRate || 0;
503
- document.getElementById('st-sdedup').textContent = (sdr * 100 >= 10 ? Math.round(sdr * 100) : (sdr * 100).toFixed(1)) + '%';
504
- document.getElementById('st-collapsed').textContent = d.store.dedupCollapsed || 0;
505
- document.getElementById('st-lastid').textContent = d.session.lastCheckpointId || 'โ€”';
506
-
507
- // ---- Repo (all sessions) โ€” same compression fields, repo scope ----------
508
- document.getElementById('rp-count').textContent = (d.repo && d.repo.checkpointCount || 0).toLocaleString();
509
- document.getElementById('rp-in').textContent = cRepo.tokensIn.toLocaleString();
510
- document.getElementById('rp-kept').textContent = cRepo.tokensOut.toLocaleString();
511
- document.getElementById('rp-freed').textContent = cRepo.tokensFreed.toLocaleString();
512
- document.getElementById('rp-sessions').textContent = (d.repo && d.repo.sessionCount || 0).toLocaleString();
513
- document.getElementById('rp-collapsed').textContent = (d.repo && d.repo.dedupCollapsed || 0).toLocaleString();
514
- var rdr = d.repo && d.repo.storageDedupRate || 0;
515
- document.getElementById('rp-sdedup').textContent = (rdr * 100 >= 10 ? Math.round(rdr * 100) : (rdr * 100).toFixed(1)) + '%';
516
- var rp = cRepo.compressionPct || 0;
517
- document.getElementById('rp-compress-bar').style.width = Math.max(rp * 100, 0.5) + '%';
518
- document.getElementById('rp-compress-bar').className = 'meter-fill ' + (rp >= 0.9 ? 'meter-green' : rp >= 0.6 ? 'meter-yellow' : 'meter-red');
519
- document.getElementById('rp-compress-sub').textContent = (rp * 100 >= 10 ? Math.round(rp * 100) : (rp * 100).toFixed(1)) + '% tokens saved ยท dedup: ' + (cRepo.dedupPct * 100 >= 10 ? Math.round(cRepo.dedupPct * 100) : (cRepo.dedupPct * 100).toFixed(1)) + '%';
520
-
521
- // Data-safety invariant (Phase 0 โ€” trust foundation).
522
- var ig = d.integrity || { regionsRetained: 0, compressedOriginalBytes: 0, duplicatesCollapsed: 0, bytesPermanentlyDeleted: 0 };
523
- function fmtBytes(b) {
524
- b = b || 0;
525
- if (b >= 1048576) return (b / 1048576).toFixed(1) + ' MiB';
526
- if (b >= 1024) return (b / 1024).toFixed(1) + ' KiB';
527
- return b + ' B';
528
- }
529
- document.getElementById('ig-retained').textContent = (ig.regionsRetained || 0).toLocaleString();
530
- document.getElementById('ig-bytes').textContent = fmtBytes(ig.compressedOriginalBytes);
531
- document.getElementById('ig-dupes').textContent = (ig.duplicatesCollapsed || 0).toLocaleString();
532
- document.getElementById('ig-deleted').textContent = fmtBytes(ig.bytesPermanentlyDeleted);
533
-
534
- // Crew / agents (live sub-agent activity + turn).
535
- var crew = d.crew || { activeAgents: 0, currentTurn: 0 };
536
- document.getElementById('cr-agents').textContent = crew.activeAgents || 0;
537
- document.getElementById('cr-turn').textContent = crew.currentTurn || 0;
538
- document.getElementById('cr-status').textContent = (crew.activeAgents > 0)
539
- ? ('โ–ถ ' + crew.activeAgents + ' running') : 'idle';
540
-
541
- // S24: headline tier is the LIVE pressure band; the config card shows the
542
- // env preset + live pressure ratio so the user sees the system react.
543
- document.getElementById('hdr-tier').textContent = d.tier;
544
- document.getElementById('cf-tier').textContent = d.tier + ' (live)';
545
- document.getElementById('cf-preset').textContent = d.presetTier;
546
- document.getElementById('cf-pressure').textContent = Math.round((d.pressure || 0) * 100) + '%';
547
- // (b) Threshold: show the effective token threshold AND the % of the model
548
- // context window it represents (percentage-based tiers). d.config.tierPct
549
- // is present on the live snapshot written by the runtime (Phase-1/2a).
550
- var cfgPct = d.config.tierPct;
551
- var cw = d.context.contextWindow || 0;
552
- var thresholdTxt = d.config.thresholdTokens.toLocaleString();
553
- if (cfgPct != null && cw > 0) {
554
- thresholdTxt += ' (' + Math.round(cfgPct * 100) + '% of ' + cw.toLocaleString() + ')';
555
- }
556
- document.getElementById('cf-threshold').textContent = thresholdTxt;
557
- // (c) Fast Gate: arming floor โ€” live trim arms once context passes this %.
558
- document.getElementById('cf-gate').textContent = d.config.fastGatePct + '%';
559
- document.getElementById('cf-auto').textContent = d.config.auto ? 'enabled' : 'disabled';
560
- document.getElementById('cf-anchor').textContent = d.config.anchorUserMessages;
561
-
562
- // --- Active model + cost savings (same calc as /mega-status) ---------------
563
- var model = d.model;
564
- document.getElementById('hdr-model').textContent = model && model.name ? model.name : 'โ€”';
565
- document.getElementById('md-name').textContent = model && model.name ? model.name : 'โ€”';
566
- document.getElementById('md-provider').textContent = model && model.providerName ? model.providerName : (model && model.provider ? model.provider : 'โ€”');
567
- document.getElementById('md-input').textContent = model && model.inputRate ? '$' + (model.inputRate).toFixed(6) : 'โ€”';
568
- document.getElementById('md-output').textContent = model && model.outputRate ? '$' + (model.outputRate).toFixed(6) : 'โ€”';
569
- var repoSaved = cRepo.tokensFreed || 0;
570
- if (model && model.inputRate && repoSaved > 0) {
571
- var usd = (repoSaved * model.inputRate);
572
- var win = d.context.contextWindow || 0;
573
- var windows = win > 0 ? (repoSaved / win).toFixed(1) : '0';
574
- document.getElementById('cost-usd').textContent = 'โ‰ˆ $' + usd.toFixed(4) + ' saved';
575
- document.getElementById('cost-windows').textContent = windows + ' context-windows extended';
576
- } else {
577
- document.getElementById('cost-usd').textContent = 'โ‰ˆ $0.00 saved';
578
- document.getElementById('cost-windows').textContent = '0 context-windows extended';
579
- }
580
-
581
- // --- Cache hits & compactions (live counters) ---------------------------
582
- var ch = d.cacheHits || { session: 0, total: 0, sessionTokensSaved: 0, totalTokensSaved: 0 };
583
- var cp = d.compacts || { session: 0, total: 0 };
584
- var ts = d.timeSaved || { compact: { sessionSec: 0, totalSec: 0 }, cacheHit: { sessionSec: 0, totalSec: 0 } };
585
- document.getElementById('ch-session').textContent = (ch.session || 0).toLocaleString();
586
- document.getElementById('ch-total').textContent = (ch.total || 0).toLocaleString();
587
- document.getElementById('ch-tok-session').textContent = (ch.sessionTokensSaved || 0).toLocaleString();
588
- document.getElementById('ch-tok-total').textContent = (ch.totalTokensSaved || 0).toLocaleString();
589
- document.getElementById('cp-session').textContent = (cp.session || 0).toLocaleString();
590
- document.getElementById('cp-total').textContent = (cp.total || 0).toLocaleString();
591
- document.getElementById('ts-compact-session').textContent = fmtSec(ts.compact.sessionSec);
592
- document.getElementById('ts-compact-total').textContent = fmtSec(ts.compact.totalSec);
593
- document.getElementById('ts-cache-session').textContent = fmtSec(ts.cacheHit.sessionSec);
594
- document.getElementById('ts-cache-total').textContent = fmtSec(ts.cacheHit.totalSec);
595
-
596
- document.getElementById('updated').textContent = 'Updated ' + new Date(d.updatedAt).toLocaleTimeString();
597
- }
598
-
599
- function sanitize(s) {
600
- return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
601
- }
602
-
603
- function renderEvent(ev) {
604
- evBuffer.unshift(ev);
605
- if (evBuffer.length > MAX_EV) evBuffer.length = MAX_EV;
606
- evBox.innerHTML = evBuffer.map(function(e) {
607
- var t = e.ts ? new Date(e.ts).toLocaleTimeString() : '';
608
- var detail = '';
609
- if (e.data) {
610
- if (e.data.checkpointId) detail = sanitize(e.data.checkpointId);
611
- else if (e.data.query) detail = sanitize(e.data.query.slice(0, 80));
612
- if (e.data.tokenEstimate != null) detail += ' ' + e.data.tokenEstimate + ' tok';
613
- if (e.data.deduped) detail += ' (deduped)';
614
- if (e.data.injected != null) detail = 'injected: ' + e.data.injected + (e.data.empty ? ' (empty)' : '');
615
- }
616
- return '<div class="ev">' +
617
- '<span class="ev-time">' + t + '</span>' +
618
- '<span class="ev-type ev-type-' + sanitize(e.type) + '">' + sanitize(e.type) + '</span>' +
619
- '<span class="ev-detail">' + detail + '</span></div>';
620
- }).join('');
621
- }
622
-
623
- // Poll snapshot every 2s
624
- function pollSnapshot() {
625
- fetch('/api/snapshot').then(function(r) { return r.json(); }).then(renderSnapshot).catch(function() {}); // guardrails-allow PREVENT-PI-004: browser-side fetch in dashboard HTML template (not Node runtime)
626
- }
627
- pollSnapshot();
628
- renderGameScores();
629
- renderAchievements();
630
- setInterval(pollSnapshot, 2000);
631
- setInterval(renderGameScores, 2000);
632
- setInterval(renderAchievements, 2000);
633
-
634
- // SSE for events
635
- function connectSSE() {
636
- var es = new EventSource('/api/events');
637
- es.onmessage = function(msg) {
638
- try { renderEvent(JSON.parse(msg.data)); } catch(e) {}
639
- };
640
- es.onerror = function() {
641
- es.close();
642
- setTimeout(connectSSE, 3000);
643
- };
644
- }
645
- connectSSE();
646
-
647
- // --- Multi-repo (index.sqlite via /api/index) ---------------------------
648
- function fmtBytesTop(b) {
649
- b = b || 0;
650
- if (b >= 1048576) return (b / 1048576).toFixed(1) + ' MiB';
651
- if (b >= 1024) return (b / 1024).toFixed(1) + ' KiB';
652
- return b + ' B';
653
- }
654
- function renderIndex(d) {
655
- d = d || { updatedAt: null, summary: null, repos: [] };
656
- var repos = d.repos || [];
657
- var s = d.summary || { totalRepos: 0, totalCheckpoints: 0, totalTokensSaved: 0, totalCompressedOriginalBytes: 0 };
658
- document.getElementById('sm-repos').textContent = (s.totalRepos || 0).toLocaleString();
659
- document.getElementById('sm-checkpoints').textContent = (s.totalCheckpoints || 0).toLocaleString();
660
- document.getElementById('sm-saved').textContent = (s.totalTokensSaved || 0).toLocaleString();
661
- document.getElementById('sm-bytes').textContent = fmtBytesTop(s.totalCompressedOriginalBytes);
662
-
663
- // Shared clickable-row renderer for both the in-current table and the
664
- // All-repos tab โ€” each row opens the per-repo detail modal.
665
- function rowsHtml() {
666
- if (!repos.length) return '<tr><td colspan="6" class="repo-none">No repositories registered yet.</td></tr>';
667
- return repos.map(function(r) {
668
- var model = r.modelName
669
- ? '<span class="repo-model">' + sanitize(r.modelName) + '</span>'
670
- : '<span class="repo-none">โ€”</span>';
671
- var when = r.lastCompactedAt ? new Date(r.lastCompactedAt).toLocaleString() : 'โ€”';
672
- return '<tr class="repo-link" data-repo="' + sanitize(r.repoRoot) + '">' +
673
- '<td title="' + sanitize(r.repoRoot) + '">' + sanitize(r.displayName || r.repoRoot) + '</td>' +
674
- '<td>' + model + '</td>' +
675
- '<td class="num">' + (r.checkpointCount || 0).toLocaleString() + '</td>' +
676
- '<td class="num">' + (r.tokensSaved || 0).toLocaleString() + '</td>' +
677
- '<td class="num">' + fmtBytesTop(r.compressedOriginalBytes) + '</td>' +
678
- '<td class="num">' + sanitize(when) + '</td>' +
679
- '</tr>';
680
- }).join('');
681
- }
682
- document.getElementById('cur-rows').innerHTML = rowsHtml();
683
- document.getElementById('all-rows').innerHTML = rowsHtml();
684
- bindRepoRows();
685
-
686
- var stamp = d.updatedAt ? 'Updated ' + new Date(d.updatedAt).toLocaleTimeString() : '';
687
- document.getElementById('cur-updated').textContent = stamp;
688
- document.getElementById('all-updated').textContent = stamp;
689
- document.getElementById('sm-updated').textContent = stamp;
690
- renderByModel(repos);
691
- }
692
-
693
- // Savings-by-model aggregation for the Summary tab โ€” groups the machine-
694
- // wide repo registry by (modelName || '(unknown)') so the user can see how
695
- // much context + cost mega-compact has reclaimed, broken down by which model
696
- // they were running. $ Saved = ฮฃ(tokensSaved ร— inputRate) per model. Sorted
697
- // by tokens saved descending so the biggest-reclaim model wins the top row.
698
- function renderByModel(repos) {
699
- var rows = document.getElementById('bm-rows');
700
- if (!rows) return;
701
- if (!repos || !repos.length) {
702
- rows.innerHTML = '<tr><td colspan="14" class="repo-none">No repositories registered yet.</td></tr>';
703
- return;
704
- }
705
- var groups = {};
706
- for (var i = 0; i < repos.length; i++) {
707
- var r = repos[i];
708
- var key = (r.modelName && String(r.modelName).trim()) || '(unknown)';
709
- if (!groups[key]) groups[key] = {
710
- model: key, provider: r.providerName || r.provider || 'โ€”', repos: 0, checkpoints: 0,
711
- tokensSaved: 0, tokensIn: 0, tokensOut: 0, sessions: 0, usd: 0, lastAt: 0,
712
- inRates: [], outRates: [], ctxWindows: [], maxTokens: [], reasoning: null,
713
- };
714
- var g = groups[key];
715
- g.repos++;
716
- g.checkpoints += (r.checkpointCount || 0);
717
- g.tokensSaved += (r.tokensSaved || 0);
718
- g.tokensIn += (r.tokensDropped || 0);
719
- g.tokensOut += (r.tokensKept || 0);
720
- g.sessions += (r.sessions || 0);
721
- if (r.inputRate) { g.usd += (r.tokensSaved || 0) * r.inputRate; g.inRates.push(r.inputRate); }
722
- if (r.outputRate) g.outRates.push(r.outputRate);
723
- if (r.contextWindow) g.ctxWindows.push(r.contextWindow);
724
- if (r.maxTokens) g.maxTokens.push(r.maxTokens);
725
- if (r.reasoning != null) g.reasoning = r.reasoning;
726
- if (r.lastCompactedAt && r.lastCompactedAt > g.lastAt) g.lastAt = r.lastCompactedAt;
727
- }
728
- var arr = [];
729
- for (var k in groups) { if (Object.prototype.hasOwnProperty.call(groups, k)) arr.push(groups[k]); }
730
- arr.sort(function(a, b) { return b.tokensSaved - a.tokensSaved; });
731
- // Helper: a set of numeric samples collapses to a single value when all
732
- // repos in the group agree, otherwise shows the range (minโ€“max) so the
733
- // user can see mixed-config model groups at a glance.
734
- function collapseNum(samples) {
735
- if (!samples || !samples.length) return 'โ€”';
736
- var lo = Math.min.apply(null, samples), hi = Math.max.apply(null, samples);
737
- return lo === hi ? lo.toLocaleString() : lo.toLocaleString() + 'โ€“' + hi.toLocaleString();
738
- }
739
- function collapseRate(samples) {
740
- if (!samples || !samples.length) return 'โ€”';
741
- var lo = Math.min.apply(null, samples), hi = Math.max.apply(null, samples);
742
- var fmt = function(v) { return '$' + v.toFixed(6); };
743
- return lo === hi ? fmt(lo) : fmt(lo) + 'โ€“' + fmt(hi);
744
- }
745
- rows.innerHTML = arr.map(function(g) {
746
- var freed = (g.tokensIn || 0) - (g.tokensOut || 0);
747
- var usd = g.usd > 0 ? '$' + g.usd.toFixed(4) : 'โ€”';
748
- var when = g.lastAt ? new Date(g.lastAt).toLocaleString() : 'โ€”';
749
- var reas = g.reasoning == null ? 'โ€”' : (g.reasoning ? 'yes' : 'no');
750
- return '<tr>' +
751
- '<td><span class="repo-model">' + sanitize(g.model) + '</span></td>' +
752
- '<td>' + sanitize(g.provider) + '</td>' +
753
- '<td class="num">' + (g.tokensIn || 0).toLocaleString() + '</td>' +
754
- '<td class="num">' + (g.tokensOut || 0).toLocaleString() + '</td>' +
755
- '<td class="num">' + freed.toLocaleString() + '</td>' +
756
- '<td class="num">' + collapseNum(g.ctxWindows) + '</td>' +
757
- '<td class="num">' + collapseNum(g.maxTokens) + '</td>' +
758
- '<td class="num">' + reas + '</td>' +
759
- '<td class="num">' + g.sessions.toLocaleString() + '</td>' +
760
- '<td class="num">' + g.checkpoints.toLocaleString() + '</td>' +
761
- '<td class="num">' + collapseRate(g.inRates) + '</td>' +
762
- '<td class="num">' + collapseRate(g.outRates) + '</td>' +
763
- '<td class="num">' + sanitize(usd) + '</td>' +
764
- '<td class="num">' + sanitize(when) + '</td>' +
765
- '</tr>';
766
- }).join('');
767
- }
768
-
769
- // Per-repo detail modal ---------------------------------------------------
770
- var detailEl = document.getElementById('repo-detail');
771
- var indexCache = { repos: [] };
772
- function openRepoDetail(root) {
773
- var r = null;
774
- for (var i = 0; i < indexCache.repos.length; i++) {
775
- if (indexCache.repos[i].repoRoot === root) { r = indexCache.repos[i]; break; }
776
- }
777
- if (!r) return;
778
- document.getElementById('rd-name').textContent = r.displayName || r.repoRoot;
779
- document.getElementById('rd-path').textContent = r.repoRoot;
780
- document.getElementById('rd-model').textContent = r.modelName || 'โ€”';
781
- document.getElementById('rd-provider').textContent = r.providerName || (r.provider || 'โ€”');
782
- document.getElementById('rd-cp').textContent = (r.checkpointCount || 0).toLocaleString();
783
- document.getElementById('rd-saved').textContent = (r.tokensSaved || 0).toLocaleString();
784
- document.getElementById('rd-bytes').textContent = fmtBytesTop(r.compressedOriginalBytes);
785
- document.getElementById('rd-when').textContent = r.lastCompactedAt ? new Date(r.lastCompactedAt).toLocaleString() : 'โ€”';
786
- detailEl.classList.add('open');
787
- }
788
- document.getElementById('rd-close').addEventListener('click', function() { detailEl.classList.remove('open'); });
789
- detailEl.addEventListener('click', function(e) { if (e.target === detailEl) detailEl.classList.remove('open'); });
790
- function bindRepoRows() {
791
- var rows = document.querySelectorAll('.repo-link');
792
- for (var i = 0; i < rows.length; i++) {
793
- rows[i].addEventListener('click', function() { openRepoDetail(this.getAttribute('data-repo')); });
794
- }
795
- }
796
- function pollIndex() {
797
- fetch('/api/index').then(function(r) { return r.json(); }).then(function(d) { // guardrails-allow PREVENT-PI-004: browser-side fetch in dashboard HTML template (not Node runtime)
798
- indexCache = d && d.repos ? d : indexCache;
799
- renderIndex(d);
800
- }).catch(function() {});
801
- }
802
- pollIndex();
803
- setInterval(pollIndex, 5000);
804
-
805
- // --- Active repos (live cache-hit / compaction stats) ---------------------
806
- function fmtSec(s) {
807
- s = s || 0;
808
- if (s >= 3600) return (s / 3600).toFixed(1) + 'h';
809
- if (s >= 60) return Math.round(s / 60) + 'm';
810
- if (s >= 1) return s.toFixed(1) + 's';
811
- return Math.round(s * 1000) + 'ms';
812
- }
813
- function renderActiveRepos(d) {
814
- d = d || { updatedAt: null, servers: [] };
815
- var servers = d.servers || [];
816
- var rowsEl = document.getElementById('active-rows');
817
- if (!rowsEl) return;
818
- if (!servers.length) {
819
- rowsEl.innerHTML = '<tr><td colspan="9" class="repo-none">No active repositories.</td></tr>';
820
- } else {
821
- rowsEl.innerHTML = servers.map(function(r) {
822
- var ch = r.cacheHits || { session: 0, total: 0, sessionTokensSaved: 0, totalTokensSaved: 0 };
823
- var cp = r.compacts || { session: 0, total: 0 };
824
- var ts = r.timeSaved || { compact: { sessionSec: 0, totalSec: 0 }, cacheHit: { sessionSec: 0, totalSec: 0 } };
825
- return '<tr>' +
826
- '<td title="' + sanitize(r.repoRoot) + '">' + sanitize(r.displayName || r.repoRoot) + '</td>' +
827
- '<td>' + sanitize(r.model || 'โ€”') + '</td>' +
828
- '<td>' + sanitize(r.tier || 'โ€”') + '</td>' +
829
- '<td class="num">' + (r.contextPct != null ? Math.round(r.contextPct * 100) + '%' : 'โ€”') + '</td>' +
830
- '<td>' + sanitize(r.state || 'โ€”') + '</td>' +
831
- '<td class="num">' + (cp.session || 0) + ' / ' + (cp.total || 0) + '</td>' +
832
- '<td class="num">' + (ch.session || 0) + ' / ' + (ch.total || 0) + '</td>' +
833
- '<td class="num">' + fmtSec(ts.compact.sessionSec) + ' / ' + fmtSec(ts.compact.totalSec) + '</td>' +
834
- '<td class="num">' + fmtSec(ts.cacheHit.sessionSec) + ' / ' + fmtSec(ts.cacheHit.totalSec) + '</td>' +
835
- '</tr>';
836
- }).join('');
837
- }
838
- var upd = document.getElementById('active-updated');
839
- if (upd) upd.textContent = d.updatedAt ? 'Updated ' + new Date(d.updatedAt).toLocaleTimeString() : '';
840
- }
841
- function pollServers() {
842
- fetch('/api/servers').then(function(r) { return r.json(); }).then(renderActiveRepos).catch(function() {}); // guardrails-allow PREVENT-PI-004: browser-side fetch in dashboard HTML template (not Node runtime)
843
- }
844
- pollServers();
845
- setInterval(pollServers, 5000);
846
-
847
- // --- Game-mode settings strip (S32) -------------------------------------
848
- // Polls GET /api/game-state and applies the row to the settings controls +
849
- // the document theme. On any control change, PUTs a partial patch back. The
850
- // dashboard server is a detached child with no MegaRuntime ref, so it reads /
851
- // writes the game_state SQLite row directly; the in-process MegaRuntime picks
852
- // up the change via its fs.watch cache-eviction watcher (S32).
853
- var gmCheckbox = document.getElementById('set-game-mode');
854
- var gmTheme = document.getElementById('set-theme');
855
- var gmTui = document.getElementById('set-tui-mode');
856
-
857
- function applyGameState(gs) {
858
- if (!gs) return;
859
- if (gs.theme) document.documentElement.dataset.theme = gs.theme;
860
- if (gmCheckbox) gmCheckbox.checked = !!gs.game_mode_on;
861
- if (gmTheme) gmTheme.value = gs.theme || 'transparent';
862
- if (gmTui) gmTui.value = gs.tui_display_mode || 'full';
863
- }
864
- function pollGameState() {
865
- fetch('/api/game-state').then(function(r) { return r.json(); }).then(applyGameState).catch(function() {}); // guardrails-allow PREVENT-PI-004: browser-side fetch in dashboard HTML template (not Node runtime)
866
- }
867
- function putGameState(patch) {
868
- fetch('/api/game-state', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch) }).then(function(r) { return r.json(); }).then(applyGameState).catch(function() {}); // guardrails-allow PREVENT-PI-004: browser-side fetch in dashboard HTML template (not Node runtime)
869
- }
870
- if (gmCheckbox) gmCheckbox.addEventListener('change', function() { putGameState({ game_mode_on: gmCheckbox.checked }); });
871
- if (gmTheme) gmTheme.addEventListener('change', function() { putGameState({ theme: gmTheme.value }); });
872
- // P3: client-side theme cycling (no new endpoint). Reads the already-fetched
873
- // <option> values from the theme select, so it stays in sync with the server's
874
- // THEMES list without a second fetch.
875
- var gmThemeNext = document.getElementById('set-theme-next');
876
- if (gmThemeNext) gmThemeNext.addEventListener('click', function() {
877
- var opts = gmTheme ? gmTheme.options : [];
878
- var n = opts.length;
879
- var cur = gmTheme ? gmTheme.value : 'transparent';
880
- var idx = -1;
881
- for (var i = 0; i < n; i++) { if (opts[i].value === cur) { idx = i; break; } }
882
- var next = n > 0 ? opts[(idx < 0 ? 0 : (idx + 1) % n)].value : 'transparent';
883
- putGameState({ theme: next });
884
- });
885
- if (gmTui) gmTui.addEventListener('change', function() { putGameState({ tui_display_mode: gmTui.value }); });
886
- pollGameState();
887
- setInterval(pollGameState, 5000);
888
-
889
- // --- Game Mode leaderboards (S34) ----------------------------------------
890
- // Polls GET /api/game-scores per metric, renders the per-repo leaderboard
891
- // tables, the MEGA CACHE banner, the hidden Opie's Wild Ride unlock tile, and
892
- // the transient oopsie toast (fires when a NEW mega_cache trophy row appears
893
- // since the last poll). Browser-side fetch only (PREVENT-PI-004).
894
- var GAME_METRICS = ['cache', 'dedupe', 'turns', 'mega_cache'];
895
- var GAME_EMPTY = true;
896
- var lastMegaTs = 0;
897
- var lastMaxLevel = 0;
898
- function fmtPct(v) { return (Math.round(v * 10) / 10) + '%'; }
899
- function fmtDate(ts) { return ts ? new Date(ts).toLocaleString() : 'โ€”'; }
900
- function trophyMeta(m) { try { return (m && typeof m === 'object') ? m : {}; } catch (e) { return {}; } }
901
- function renderGameScores() {
902
- var results = {};
903
- var pending = GAME_METRICS.length + 1; // metrics + repos badge
904
- function done() {
905
- if (--pending > 0) return;
906
- var cache = results['cache'] || [];
907
- if (cache.length) GAME_EMPTY = false;
908
- document.getElementById('lb-cache').innerHTML = cache.map(function(r) {
909
- return '<tr><td title="' + sanitize(r.repo_root) + '">' + sanitize(r.repo_root.split('/').pop()) + '</td><td class="num">' + sanitize(String(r.value)) + '</td></tr>';
910
- }).join('') || '<tr><td colspan="2" class="repo-none">no data</td></tr>';
911
- var dedupe = results['dedupe'] || [];
912
- if (dedupe.length) GAME_EMPTY = false;
913
- document.getElementById('lb-dedupe').innerHTML = dedupe.map(function(r) {
914
- return '<tr><td title="' + sanitize(r.repo_root) + '">' + sanitize(r.repo_root.split('/').pop()) + '</td><td class="num">' + sanitize(String(r.value)) + '</td></tr>';
915
- }).join('') || '<tr><td colspan="2" class="repo-none">no data</td></tr>';
916
- var turns = results['turns'] || [];
917
- var maxTurns = turns.reduce(function(mx, r) { return Math.max(mx, r.value); }, 0);
918
- var lvl = Math.floor(Math.log2(maxTurns + 1)) + 1;
919
- var lvlEl = document.getElementById('turns-level');
920
- if (lvlEl) lvlEl.textContent = 'LVL ' + lvl;
921
- if (turns.length) GAME_EMPTY = false;
922
- // level-up pulse: when max level increases vs last poll, pulse the cache bar
923
- if (lvl > lastMaxLevel && lastMaxLevel > 0) {
924
- var bar = document.getElementById('ctx-bar');
925
- if (bar) { bar.classList.add('level-up'); setTimeout(function() { bar.classList.remove('level-up'); }, 1200); }
926
- }
927
- if (lvl > lastMaxLevel) lastMaxLevel = lvl;
928
- document.getElementById('lb-turns').innerHTML = turns.map(function(r) {
929
- return '<tr><td title="' + sanitize(r.repo_root) + '">' + sanitize(r.repo_root.split('/').pop()) + '</td><td class="num">' + sanitize(String(r.value)) + '</td></tr>';
930
- }).join('') || '<tr><td colspan="2" class="repo-none">no data</td></tr>';
931
- // mega_cache trophies + banner + Opie tile + transient toast
932
- var mega = results['mega_cache'] || [];
933
- var megaBody = document.getElementById('lb-mega_cache');
934
- if (megaBody) {
935
- megaBody.innerHTML = mega.map(function(r) {
936
- var m = trophyMeta(r.meta);
937
- var fs = m.firstSeenTs || m.firstSeen || r.ts;
938
- var extra = fs ? ' <span class="lb-meta">' + sanitize(fmtDate(fs)) + '</span>' : '';
939
- return '<tr><td title="' + sanitize(r.repo_root) + '">' + sanitize(r.repo_root.split('/').pop()) + '</td><td class="num">' + sanitize(fmtPct(r.value)) + extra + '</td></tr>';
940
- }).join('') || '<tr><td colspan="2" class="repo-none">no trophies yet</td></tr>';
941
- }
942
- var banner = document.getElementById('mega-cache-banner');
943
- var tile = document.getElementById('opie-tile');
944
- var best = null, firstSeen = null;
945
- mega.forEach(function(r) {
946
- if (best == null || r.value > best) best = r.value;
947
- var m = trophyMeta(r.meta);
948
- var fs = m.firstSeenTs || m.firstSeen || r.ts;
949
- if (firstSeen == null || fs < firstSeen) firstSeen = fs;
950
- });
951
- if (banner) {
952
- if (best != null && best > 100) {
953
- banner.style.display = 'block';
954
- banner.textContent = '๐Ÿฅง MEGA CACHE! peak ' + fmtPct(best) + ' โ€” first reached ' + fmtDate(firstSeen);
955
- } else { banner.style.display = 'none'; }
956
- }
957
- if (tile) {
958
- if (best != null && best > 100) {
959
- tile.style.display = 'block';
960
- tile.className = 'achievement-tile unlocked';
961
- tile.innerHTML = '๐Ÿ† Opie\\'s Wild Ride<span class="ach-detail">best ' + sanitize(fmtPct(best)) + ' ยท first ' + sanitize(fmtDate(firstSeen)) + '</span>';
962
- } else { tile.style.display = 'none'; tile.className = 'achievement-tile'; }
963
- }
964
- // transient oopsie toast: a NEW mega_cache trophy row since the last poll
965
- var maxTs = mega.reduce(function(mx, r) { return Math.max(mx, r.ts); }, 0);
966
- var newRow = mega.find(function(r) { return r.ts > lastMegaTs && r.value > 100; });
967
- if (lastMegaTs && newRow) {
968
- var toast = document.getElementById('mega-cache-toast');
969
- if (toast) {
970
- toast.textContent = 'oopsie! cache went to ' + Math.round(newRow.value) + '% โ€” MEGA CACHE ๐Ÿฅง';
971
- toast.classList.add('show');
972
- setTimeout(function() { toast.classList.remove('show'); }, 4000);
973
- }
974
- }
975
- if (maxTs > lastMegaTs) lastMegaTs = maxTs;
976
- // empty state
977
- var emptyEl = document.getElementById('game-empty');
978
- if (emptyEl) emptyEl.style.display = GAME_EMPTY ? 'block' : 'none';
979
- }
980
- GAME_METRICS.forEach(function(m) {
981
- fetch('/api/game-scores?metric=' + encodeURIComponent(m) + '&limit=25').then(function(r) { return r.ok ? r.json() : []; }).then(function(rows) { results[m] = rows || []; }).catch(function() { results[m] = []; }).then(done);
982
- });
983
- fetch('/api/game-scores?metric=repos&limit=1').then(function(r) { return r.ok ? r.json() : []; }).then(function(rows) {
984
- var badge = document.getElementById('repos-badge');
985
- if (badge) badge.textContent = ((rows && rows.length) ? rows[0].value : 0) + ' repos';
986
- if (rows && rows.length && rows[0].value > 0) GAME_EMPTY = false;
987
- }).catch(function() {}).then(done);
988
- }
989
-
990
- // --- Achievements tile row (S35) ------------------------------------------
991
- // Polls GET /api/achievements; renders the tile row (hidden+locked render
992
- // NOTHING; unlocked show icon+title+date; visible-but-locked show ??? teaser)
993
- // and fires a transient toast when a newly-unlocked achievement appears.
994
- // Browser-side fetch only (PREVENT-PI-004).
995
- var lastAchMaxTs = 0;
996
- function renderAchievements() {
997
- fetch('/api/achievements').then(function(r) { return r.ok ? r.json() : []; }).then(function(rows) { // guardrails-allow PREVENT-PI-004: browser-side fetch in dashboard HTML template (not Node runtime)
998
- var box = document.getElementById('ach-tiles');
999
- if (!box) return;
1000
- var html = '';
1001
- var maxTs = 0;
1002
- (rows || []).forEach(function(a) {
1003
- if (a.hidden === 1 && a.unlocked_at == null) return; // hidden invariant: render nothing
1004
- if (a.unlocked_at != null) {
1005
- var isNew = a.unlocked_at > lastAchMaxTs && lastAchMaxTs > 0;
1006
- maxTs = Math.max(maxTs, a.unlocked_at);
1007
- html += '<div class="ach-tile unlocked' + (isNew ? ' just-unlocked' : '') + '">' + sanitize(a.icon || '') + ' ' + sanitize(a.title) + '<span class="ach-detail">unlocked ' + sanitize(fmtDate(a.unlocked_at)) + '</span></div>';
1008
- } else {
1009
- html += '<div class="ach-tile locked">??? ' + sanitize(a.title) + '</div>';
1010
- }
1011
- });
1012
- box.innerHTML = html || '<span class="repo-none">no achievements yet</span>';
1013
- var newly = (rows || []).filter(function(a) { return a.unlocked_at != null && a.unlocked_at > lastAchMaxTs; });
1014
- if (lastAchMaxTs && newly.length) {
1015
- var toast = document.getElementById('ach-toast');
1016
- if (toast) {
1017
- toast.textContent = newly.map(function(a) { return (a.icon || '') + ' ' + a.title; }).join(', ') + ' unlocked!';
1018
- toast.classList.add('show');
1019
- setTimeout(function() { toast.classList.remove('show'); }, 4000);
1020
- }
1021
- }
1022
- if (maxTs > lastAchMaxTs) lastAchMaxTs = maxTs;
1023
- }).catch(function() {});
1024
- }
1025
-
1026
- // --- Perf tab (v0.8.8) โ€” live local instrumentation ----------------------
1027
- var perfPollTimer = null;
1028
- function pollPerf() {
1029
- fetch('/api/perf?minutes=30').then(function(r) { return r.ok ? r.json() : null; }).then(function(d) { // guardrails-allow PREVENT-PI-004: browser-side fetch in dashboard HTML template (not Node runtime)
1030
- if (!d) return;
1031
- var el = document.getElementById('perf-updated');
1032
- if (el) el.textContent = d.sampleCount + ' samples \u00b7 updated ' + (d.updatedAt || '');
1033
- function setText(id, txt) { var e = document.getElementById(id); if (e) e.textContent = txt; }
1034
- function fmtMs(v) { return v == null ? '\u2014' : (v >= 100 ? Math.round(v) + 'ms' : v.toFixed(1) + 'ms'); }
1035
- function fmtNum(v, dec) { return v == null ? '\u2014' : (typeof v === 'number' ? v.toFixed(dec) : '\u2014'); }
1036
- setText('pf-turn-p50', fmtMs(d.turn_latency_ms && d.turn_latency_ms.p50));
1037
- setText('pf-turn-p95', fmtMs(d.turn_latency_ms && d.turn_latency_ms.p95));
1038
- setText('pf-prov-p50', fmtMs(d.provider_latency_ms && d.provider_latency_ms.p50));
1039
- setText('pf-prov-p95', fmtMs(d.provider_latency_ms && d.provider_latency_ms.p95));
1040
- setText('pf-tps', fmtNum(d.tps && d.tps.avg, 1));
1041
- setText('pf-cache', (d.cache_hit_pct && typeof d.cache_hit_pct.avg === 'number') ? fmtNum(d.cache_hit_pct.avg, 1) + '%' : '\u2014');
1042
- setText('pf-rss', (d.rss_mb && typeof d.rss_mb.latest === 'number') ? fmtNum(d.rss_mb.latest, 1) + ' MB' : '\u2014');
1043
- setText('pf-heap', (d.heap_mb && typeof d.heap_mb.latest === 'number') ? fmtNum(d.heap_mb.latest, 1) + ' MB' : '\u2014');
1044
- setText('pf-cpu', (d.cpu_user_ms && d.cpu_sys_ms) ? (fmtNum(d.cpu_user_ms.latest,1) + ' / ' + fmtNum(d.cpu_sys_ms.latest,1) + ' ms') : '\u2014');
1045
- setText('pf-db-p50', fmtMs(d.db_recompute_ms && d.db_recompute_ms.p50));
1046
- setText('pf-db-p95', fmtMs(d.db_recompute_ms && d.db_recompute_ms.p95));
1047
- setText('pf-disk', fmtMs(d.disk_write_ms && d.disk_write_ms.p50));
1048
- var diag = d.diag || {};
1049
- setText('pf-recompute', diag.liveTrimFires != null ? String(diag.liveTrimFires) : '\u2014');
1050
- setText('pf-replays', diag.liveTrimReplays != null ? String(diag.liveTrimReplays) : '\u2014');
1051
- setText('pf-skips', diag.ctxFastGate != null ? String(diag.ctxFastGate) : '\u2014');
1052
- }).catch(function() {});
1053
- }
1054
- function startPerfPoll() { if (perfPollTimer) return; pollPerf(); perfPollTimer = setInterval(pollPerf, 2000); }
1055
- function stopPerfPoll() { if (perfPollTimer) { clearInterval(perfPollTimer); perfPollTimer = null; } }
1056
-
1057
- // --- Tab switching ------------------------------------------------------
1058
- var tabs = document.querySelectorAll('.tab');
1059
- var panels = { current: 'panel-current', all: 'panel-all', active: 'panel-active', summary: 'panel-summary', game: 'panel-game', perf: 'panel-perf' };
1060
- for (var i = 0; i < tabs.length; i++) {
1061
- tabs[i].addEventListener('click', function() {
1062
- var name = this.getAttribute('data-tab');
1063
- for (var j = 0; j < tabs.length; j++) tabs[j].classList.remove('active');
1064
- this.classList.add('active');
1065
- for (var k in panels) {
1066
- if (Object.prototype.hasOwnProperty.call(panels, k)) {
1067
- var el = document.getElementById(panels[k]);
1068
- if (el) el.classList.toggle('active', k === name);
1069
- }
1070
- }
1071
- if (name === 'all' || name === 'summary') pollIndex();
1072
- if (name === 'active') pollServers();
1073
- if (name === 'game') { renderGameScores(); renderAchievements(); }
1074
- if (name === 'perf') startPerfPoll(); else stopPerfPoll();
1075
- });
1076
- }
1077
- })();
458
+ ${dashboardClientJs()}
1078
459
  </script>
1079
460
  </body>
1080
461
  </html>`;