pi-mega-compact 0.8.21 → 0.8.22

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 (76) hide show
  1. package/dist/extensions/dashboard-server/dashboard-client-core.js +201 -0
  2. package/dist/extensions/dashboard-server/dashboard-client-game.js +241 -0
  3. package/dist/extensions/dashboard-server/dashboard-client-repos.js +212 -0
  4. package/dist/extensions/dashboard-server/dashboard-client.js +19 -0
  5. package/dist/extensions/dashboard-server/html.js +2 -621
  6. package/dist/extensions/dashboard-server/routes-core.js +62 -0
  7. package/dist/extensions/dashboard-server/routes-game.js +323 -0
  8. package/dist/extensions/dashboard-server/routes-repo.js +170 -0
  9. package/dist/extensions/dashboard-server/routes-sessions.js +159 -0
  10. package/dist/extensions/dashboard-server/routes.js +10 -0
  11. package/dist/extensions/dashboard-server/server.js +26 -623
  12. package/dist/extensions/mega-commands.js +4 -3
  13. package/dist/extensions/mega-events/agent-handlers.js +2 -1
  14. package/dist/extensions/mega-events/compact-handlers.js +26 -0
  15. package/dist/extensions/mega-events/session-handlers.js +2 -1
  16. package/dist/extensions/mega-pipeline/compact.js +3 -2
  17. package/dist/extensions/mega-runtime/state.js +7 -7
  18. package/dist/src/dedup/raptor/promote.test.js +5 -5
  19. package/dist/src/dedup/sprint12.test.js +7 -7
  20. package/dist/src/dedup-engine.test.js +29 -29
  21. package/dist/src/e2e.test.js +38 -38
  22. package/dist/src/engine.js +3 -3
  23. package/dist/src/engine.test.js +6 -6
  24. package/dist/src/importance.js +197 -0
  25. package/dist/src/importance.test.js +372 -0
  26. package/dist/src/ratio.bench.test.js +18 -18
  27. package/dist/src/recall.js +6 -5
  28. package/dist/src/recall.test.js +85 -27
  29. package/dist/src/sprint14.test.js +2 -2
  30. package/dist/src/store/migrate.test.js +5 -5
  31. package/dist/src/store/sprint10.test.js +5 -5
  32. package/dist/src/store/sqlite/global-index.js +5 -174
  33. package/dist/src/store/sqlite/global-sessions.js +190 -0
  34. package/dist/src/vector-read.js +168 -0
  35. package/dist/src/vector-search.js +191 -0
  36. package/dist/src/vectorStore.js +10 -297
  37. package/dist/src/vectorStore.test.js +32 -32
  38. package/extensions/dashboard-server/dashboard-client-core.ts +202 -0
  39. package/extensions/dashboard-server/dashboard-client-game.ts +242 -0
  40. package/extensions/dashboard-server/dashboard-client-repos.ts +213 -0
  41. package/extensions/dashboard-server/dashboard-client.ts +21 -0
  42. package/extensions/dashboard-server/html.ts +2 -621
  43. package/extensions/dashboard-server/routes-core.ts +113 -0
  44. package/extensions/dashboard-server/routes-game.ts +386 -0
  45. package/extensions/dashboard-server/routes-repo.ts +212 -0
  46. package/extensions/dashboard-server/routes-sessions.ts +195 -0
  47. package/extensions/dashboard-server/routes.ts +13 -0
  48. package/extensions/dashboard-server/server.ts +37 -700
  49. package/extensions/mega-commands.ts +4 -3
  50. package/extensions/mega-events/agent-handlers.ts +2 -1
  51. package/extensions/mega-events/compact-handlers.ts +28 -0
  52. package/extensions/mega-events/session-handlers.ts +2 -1
  53. package/extensions/mega-pipeline/compact.ts +3 -2
  54. package/extensions/mega-runtime/state.ts +7 -7
  55. package/extensions/openclaw-mega-compact.ts +2 -2
  56. package/package.json +1 -1
  57. package/src/dedup/raptor/promote.test.ts +5 -5
  58. package/src/dedup/sprint12.test.ts +7 -7
  59. package/src/dedup-engine.test.ts +30 -30
  60. package/src/e2e.test.ts +38 -38
  61. package/src/engine.test.ts +6 -6
  62. package/src/engine.ts +3 -3
  63. package/src/importance.test.ts +538 -0
  64. package/src/importance.ts +312 -0
  65. package/src/ratio.bench.test.ts +18 -18
  66. package/src/recall.test.ts +101 -29
  67. package/src/recall.ts +9 -9
  68. package/src/sprint14.test.ts +2 -2
  69. package/src/store/migrate.test.ts +5 -5
  70. package/src/store/sprint10.test.ts +5 -5
  71. package/src/store/sqlite/global-index.ts +18 -290
  72. package/src/store/sqlite/global-sessions.ts +291 -0
  73. package/src/vector-read.ts +237 -0
  74. package/src/vector-search.ts +231 -0
  75. package/src/vectorStore.test.ts +32 -32
  76. package/src/vectorStore.ts +29 -356
@@ -0,0 +1,201 @@
1
+ /**
2
+ * dashboard-client-core.ts — verbatim JS chunk: snapshot + SSE + event rendering.
3
+ *
4
+ * Extracted from html.ts (PR0 split) — ZERO behavior change. This is one of
5
+ * three chunks whose concatenation forms the dashboard's IIFE client script
6
+ * (see dashboard-client.ts). The chunks share one closure, so they must be
7
+ * concatenated in order: core → repos → game.
8
+ */
9
+ /** Snapshot rendering, event/SSE handling, and shared helpers. */
10
+ export function dashboardClientCoreJs() {
11
+ return `var evBox = document.getElementById('events');
12
+ var evBuffer = [];
13
+ var MAX_EV = 50;
14
+ var offlineBanner = document.getElementById('offline-banner');
15
+
16
+ function bullet(el, on, na) {
17
+ el.className = 'bullet ' + (na ? 'bullet-na' : on ? 'bullet-on' : 'bullet-off');
18
+ }
19
+
20
+ function renderSnapshot(d) {
21
+ if (!d || !d.updatedAt) { offlineBanner.style.display = 'block'; return; }
22
+ offlineBanner.style.display = 'none';
23
+
24
+ var pct = d.context.percent || 0;
25
+ document.getElementById('ctx-pct').textContent = pct + '%';
26
+ var bar = document.getElementById('ctx-bar');
27
+ bar.style.width = Math.max(pct, 1) + '%';
28
+ bar.className = 'meter-fill ' + (pct >= 90 ? 'meter-red' : pct >= 70 ? 'meter-yellow' : 'meter-green');
29
+ var tok = d.context.tokens != null ? d.context.tokens.toLocaleString() : '?';
30
+ var win = d.context.contextWindow ? d.context.contextWindow.toLocaleString() : '?';
31
+ document.getElementById('ctx-sub').textContent = tok + ' / ' + win + ' tokens';
32
+
33
+ bullet(document.getElementById('tr-armed'), d.trigger.armed, false);
34
+ bullet(document.getElementById('tr-ready'), d.trigger.ready, !d.trigger.armed);
35
+ var state = d.trigger.ready ? 'THRESHOLD EXCEEDED — compacting next event' :
36
+ d.trigger.armed ? 'past fast gate — monitoring token count' : 'idle — below fast gate';
37
+ document.getElementById('tr-state').textContent = state;
38
+
39
+ // ---- Vector Store — reconciled token accounting (same formula as widget) -
40
+ document.getElementById('st-count').textContent = d.store.checkpointCount;
41
+ // Compression block from the snapshot (Freed = In − Out, single formula).
42
+ var c = d.compression || {};
43
+ var sess = c.session || { tokensIn:0, tokensOut:0, tokensFreed:0, compressionPct:0, dedupPct:0 };
44
+ var cRepo = c.repo || { tokensIn:0, tokensOut:0, tokensFreed:0, compressionPct:0, dedupPct:0 };
45
+ document.getElementById('st-in').textContent = sess.tokensIn.toLocaleString();
46
+ document.getElementById('st-kept').textContent = sess.tokensOut.toLocaleString();
47
+ document.getElementById('st-freed').textContent = sess.tokensFreed.toLocaleString();
48
+ var sp = sess.compressionPct || 0;
49
+ document.getElementById('st-compress-bar').style.width = Math.max(sp * 100, 0.5) + '%';
50
+ document.getElementById('st-compress-bar').className = 'meter-fill ' + (sp >= 0.9 ? 'meter-green' : sp >= 0.6 ? 'meter-yellow' : 'meter-red');
51
+ 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)) + '%';
52
+ // ------
53
+ document.getElementById('st-injected').textContent = d.store.injectedCount;
54
+ document.getElementById('st-dedup').textContent = Math.round(d.store.dedupHitRate * 100) + '%';
55
+ var sdr = d.store.storageDedupRate || 0;
56
+ document.getElementById('st-sdedup').textContent = (sdr * 100 >= 10 ? Math.round(sdr * 100) : (sdr * 100).toFixed(1)) + '%';
57
+ document.getElementById('st-collapsed').textContent = d.store.dedupCollapsed || 0;
58
+ document.getElementById('st-lastid').textContent = d.session.lastCheckpointId || '—';
59
+
60
+ // ---- Repo (all sessions) — same compression fields, repo scope ----------
61
+ document.getElementById('rp-count').textContent = (d.repo && d.repo.checkpointCount || 0).toLocaleString();
62
+ document.getElementById('rp-in').textContent = cRepo.tokensIn.toLocaleString();
63
+ document.getElementById('rp-kept').textContent = cRepo.tokensOut.toLocaleString();
64
+ document.getElementById('rp-freed').textContent = cRepo.tokensFreed.toLocaleString();
65
+ document.getElementById('rp-sessions').textContent = (d.repo && d.repo.sessionCount || 0).toLocaleString();
66
+ document.getElementById('rp-collapsed').textContent = (d.repo && d.repo.dedupCollapsed || 0).toLocaleString();
67
+ var rdr = d.repo && d.repo.storageDedupRate || 0;
68
+ document.getElementById('rp-sdedup').textContent = (rdr * 100 >= 10 ? Math.round(rdr * 100) : (rdr * 100).toFixed(1)) + '%';
69
+ var rp = cRepo.compressionPct || 0;
70
+ document.getElementById('rp-compress-bar').style.width = Math.max(rp * 100, 0.5) + '%';
71
+ document.getElementById('rp-compress-bar').className = 'meter-fill ' + (rp >= 0.9 ? 'meter-green' : rp >= 0.6 ? 'meter-yellow' : 'meter-red');
72
+ 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)) + '%';
73
+
74
+ // Data-safety invariant (Phase 0 — trust foundation).
75
+ var ig = d.integrity || { regionsRetained: 0, compressedOriginalBytes: 0, duplicatesCollapsed: 0, bytesPermanentlyDeleted: 0 };
76
+ function fmtBytes(b) {
77
+ b = b || 0;
78
+ if (b >= 1048576) return (b / 1048576).toFixed(1) + ' MiB';
79
+ if (b >= 1024) return (b / 1024).toFixed(1) + ' KiB';
80
+ return b + ' B';
81
+ }
82
+ document.getElementById('ig-retained').textContent = (ig.regionsRetained || 0).toLocaleString();
83
+ document.getElementById('ig-bytes').textContent = fmtBytes(ig.compressedOriginalBytes);
84
+ document.getElementById('ig-dupes').textContent = (ig.duplicatesCollapsed || 0).toLocaleString();
85
+ document.getElementById('ig-deleted').textContent = fmtBytes(ig.bytesPermanentlyDeleted);
86
+
87
+ // Crew / agents (live sub-agent activity + turn).
88
+ var crew = d.crew || { activeAgents: 0, currentTurn: 0 };
89
+ document.getElementById('cr-agents').textContent = crew.activeAgents || 0;
90
+ document.getElementById('cr-turn').textContent = crew.currentTurn || 0;
91
+ document.getElementById('cr-status').textContent = (crew.activeAgents > 0)
92
+ ? ('▶ ' + crew.activeAgents + ' running') : 'idle';
93
+
94
+ // S24: headline tier is the LIVE pressure band; the config card shows the
95
+ // env preset + live pressure ratio so the user sees the system react.
96
+ document.getElementById('hdr-tier').textContent = d.tier;
97
+ document.getElementById('cf-tier').textContent = d.tier + ' (live)';
98
+ document.getElementById('cf-preset').textContent = d.presetTier;
99
+ document.getElementById('cf-pressure').textContent = Math.round((d.pressure || 0) * 100) + '%';
100
+ // (b) Threshold: show the effective token threshold AND the % of the model
101
+ // context window it represents (percentage-based tiers). d.config.tierPct
102
+ // is present on the live snapshot written by the runtime (Phase-1/2a).
103
+ var cfgPct = d.config.tierPct;
104
+ var cw = d.context.contextWindow || 0;
105
+ var thresholdTxt = d.config.thresholdTokens.toLocaleString();
106
+ if (cfgPct != null && cw > 0) {
107
+ thresholdTxt += ' (' + Math.round(cfgPct * 100) + '% of ' + cw.toLocaleString() + ')';
108
+ }
109
+ document.getElementById('cf-threshold').textContent = thresholdTxt;
110
+ // (c) Fast Gate: arming floor — live trim arms once context passes this %.
111
+ document.getElementById('cf-gate').textContent = d.config.fastGatePct + '%';
112
+ document.getElementById('cf-auto').textContent = d.config.auto ? 'enabled' : 'disabled';
113
+ document.getElementById('cf-anchor').textContent = d.config.anchorUserMessages;
114
+
115
+ // --- Active model + cost savings (same calc as /mega-status) ---------------
116
+ var model = d.model;
117
+ document.getElementById('hdr-model').textContent = model && model.name ? model.name : '—';
118
+ document.getElementById('md-name').textContent = model && model.name ? model.name : '—';
119
+ document.getElementById('md-provider').textContent = model && model.providerName ? model.providerName : (model && model.provider ? model.provider : '—');
120
+ document.getElementById('md-input').textContent = model && model.inputRate ? '$' + (model.inputRate).toFixed(6) : '—';
121
+ document.getElementById('md-output').textContent = model && model.outputRate ? '$' + (model.outputRate).toFixed(6) : '—';
122
+ var repoSaved = cRepo.tokensFreed || 0;
123
+ if (model && model.inputRate && repoSaved > 0) {
124
+ var usd = (repoSaved * model.inputRate);
125
+ var win = d.context.contextWindow || 0;
126
+ var windows = win > 0 ? (repoSaved / win).toFixed(1) : '0';
127
+ document.getElementById('cost-usd').textContent = '≈ $' + usd.toFixed(4) + ' saved';
128
+ document.getElementById('cost-windows').textContent = windows + ' context-windows extended';
129
+ } else {
130
+ document.getElementById('cost-usd').textContent = '≈ $0.00 saved';
131
+ document.getElementById('cost-windows').textContent = '0 context-windows extended';
132
+ }
133
+
134
+ // --- Cache hits & compactions (live counters) ---------------------------
135
+ var ch = d.cacheHits || { session: 0, total: 0, sessionTokensSaved: 0, totalTokensSaved: 0 };
136
+ var cp = d.compacts || { session: 0, total: 0 };
137
+ var ts = d.timeSaved || { compact: { sessionSec: 0, totalSec: 0 }, cacheHit: { sessionSec: 0, totalSec: 0 } };
138
+ document.getElementById('ch-session').textContent = (ch.session || 0).toLocaleString();
139
+ document.getElementById('ch-total').textContent = (ch.total || 0).toLocaleString();
140
+ document.getElementById('ch-tok-session').textContent = (ch.sessionTokensSaved || 0).toLocaleString();
141
+ document.getElementById('ch-tok-total').textContent = (ch.totalTokensSaved || 0).toLocaleString();
142
+ document.getElementById('cp-session').textContent = (cp.session || 0).toLocaleString();
143
+ document.getElementById('cp-total').textContent = (cp.total || 0).toLocaleString();
144
+ document.getElementById('ts-compact-session').textContent = fmtSec(ts.compact.sessionSec);
145
+ document.getElementById('ts-compact-total').textContent = fmtSec(ts.compact.totalSec);
146
+ document.getElementById('ts-cache-session').textContent = fmtSec(ts.cacheHit.sessionSec);
147
+ document.getElementById('ts-cache-total').textContent = fmtSec(ts.cacheHit.totalSec);
148
+
149
+ document.getElementById('updated').textContent = 'Updated ' + new Date(d.updatedAt).toLocaleTimeString();
150
+ }
151
+
152
+ function sanitize(s) {
153
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
154
+ }
155
+
156
+ function renderEvent(ev) {
157
+ evBuffer.unshift(ev);
158
+ if (evBuffer.length > MAX_EV) evBuffer.length = MAX_EV;
159
+ evBox.innerHTML = evBuffer.map(function(e) {
160
+ var t = e.ts ? new Date(e.ts).toLocaleTimeString() : '';
161
+ var detail = '';
162
+ if (e.data) {
163
+ if (e.data.checkpointId) detail = sanitize(e.data.checkpointId);
164
+ else if (e.data.query) detail = sanitize(e.data.query.slice(0, 80));
165
+ if (e.data.tokenEstimate != null) detail += ' ' + e.data.tokenEstimate + ' tok';
166
+ if (e.data.deduped) detail += ' (deduped)';
167
+ if (e.data.injected != null) detail = 'injected: ' + e.data.injected + (e.data.empty ? ' (empty)' : '');
168
+ }
169
+ return '<div class="ev">' +
170
+ '<span class="ev-time">' + t + '</span>' +
171
+ '<span class="ev-type ev-type-' + sanitize(e.type) + '">' + sanitize(e.type) + '</span>' +
172
+ '<span class="ev-detail">' + detail + '</span></div>';
173
+ }).join('');
174
+ }
175
+
176
+ // Poll snapshot every 2s
177
+ function pollSnapshot() {
178
+ 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)
179
+ }
180
+ pollSnapshot();
181
+ renderGameScores();
182
+ renderAchievements();
183
+ setInterval(pollSnapshot, 2000);
184
+ setInterval(renderGameScores, 2000);
185
+ setInterval(renderAchievements, 2000);
186
+
187
+ // SSE for events
188
+ function connectSSE() {
189
+ var es = new EventSource('/api/events');
190
+ es.onmessage = function(msg) {
191
+ try { renderEvent(JSON.parse(msg.data)); } catch(e) {}
192
+ };
193
+ es.onerror = function() {
194
+ es.close();
195
+ setTimeout(connectSSE, 3000);
196
+ };
197
+ }
198
+ connectSSE();
199
+
200
+ `;
201
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * dashboard-client-game.ts — verbatim JS chunk: game-mode, achievements, perf, tabs.
3
+ *
4
+ * Extracted from html.ts (PR0 split) — ZERO behavior change. Third of three
5
+ * chunks concatenated into the dashboard IIFE (core → repos → game). Shares
6
+ * the closure with core + repos.
7
+ */
8
+ /** Game-mode settings, leaderboards, achievements, perf tab, tab switching. */
9
+ export function dashboardClientGameJs() {
10
+ return `// --- Game-mode settings strip (S32) -------------------------------------
11
+ // Polls GET /api/game-state and applies the row to the settings controls +
12
+ // the document theme. On any control change, PUTs a partial patch back. The
13
+ // dashboard server is a detached child with no MegaRuntime ref, so it reads /
14
+ // writes the game_state SQLite row directly; the in-process MegaRuntime picks
15
+ // up the change via its fs.watch cache-eviction watcher (S32).
16
+ var gmCheckbox = document.getElementById('set-game-mode');
17
+ var gmTheme = document.getElementById('set-theme');
18
+ var gmTui = document.getElementById('set-tui-mode');
19
+
20
+ function applyGameState(gs) {
21
+ if (!gs) return;
22
+ if (gs.theme) document.documentElement.dataset.theme = gs.theme;
23
+ if (gmCheckbox) gmCheckbox.checked = !!gs.game_mode_on;
24
+ if (gmTheme) gmTheme.value = gs.theme || 'transparent';
25
+ if (gmTui) gmTui.value = gs.tui_display_mode || 'full';
26
+ }
27
+ function pollGameState() {
28
+ 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)
29
+ }
30
+ function putGameState(patch) {
31
+ 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)
32
+ }
33
+ if (gmCheckbox) gmCheckbox.addEventListener('change', function() { putGameState({ game_mode_on: gmCheckbox.checked }); });
34
+ if (gmTheme) gmTheme.addEventListener('change', function() { putGameState({ theme: gmTheme.value }); });
35
+ // P3: client-side theme cycling (no new endpoint). Reads the already-fetched
36
+ // <option> values from the theme select, so it stays in sync with the server's
37
+ // THEMES list without a second fetch.
38
+ var gmThemeNext = document.getElementById('set-theme-next');
39
+ if (gmThemeNext) gmThemeNext.addEventListener('click', function() {
40
+ var opts = gmTheme ? gmTheme.options : [];
41
+ var n = opts.length;
42
+ var cur = gmTheme ? gmTheme.value : 'transparent';
43
+ var idx = -1;
44
+ for (var i = 0; i < n; i++) { if (opts[i].value === cur) { idx = i; break; } }
45
+ var next = n > 0 ? opts[(idx < 0 ? 0 : (idx + 1) % n)].value : 'transparent';
46
+ putGameState({ theme: next });
47
+ });
48
+ if (gmTui) gmTui.addEventListener('change', function() { putGameState({ tui_display_mode: gmTui.value }); });
49
+ pollGameState();
50
+ setInterval(pollGameState, 5000);
51
+
52
+ // --- Game Mode leaderboards (S34) ----------------------------------------
53
+ // Polls GET /api/game-scores per metric, renders the per-repo leaderboard
54
+ // tables, the MEGA CACHE banner, the hidden Opie's Wild Ride unlock tile, and
55
+ // the transient oopsie toast (fires when a NEW mega_cache trophy row appears
56
+ // since the last poll). Browser-side fetch only (PREVENT-PI-004).
57
+ var GAME_METRICS = ['cache', 'dedupe', 'turns', 'mega_cache'];
58
+ var GAME_EMPTY = true;
59
+ var lastMegaTs = 0;
60
+ var lastMaxLevel = 0;
61
+ function fmtPct(v) { return (Math.round(v * 10) / 10) + '%'; }
62
+ function fmtDate(ts) { return ts ? new Date(ts).toLocaleString() : '—'; }
63
+ function trophyMeta(m) { try { return (m && typeof m === 'object') ? m : {}; } catch (e) { return {}; } }
64
+ function renderGameScores() {
65
+ var results = {};
66
+ var pending = GAME_METRICS.length + 1; // metrics + repos badge
67
+ function done() {
68
+ if (--pending > 0) return;
69
+ var cache = results['cache'] || [];
70
+ if (cache.length) GAME_EMPTY = false;
71
+ document.getElementById('lb-cache').innerHTML = cache.map(function(r) {
72
+ return '<tr><td title="' + sanitize(r.repo_root) + '">' + sanitize(r.repo_root.split('/').pop()) + '</td><td class="num">' + sanitize(String(r.value)) + '</td></tr>';
73
+ }).join('') || '<tr><td colspan="2" class="repo-none">no data</td></tr>';
74
+ var dedupe = results['dedupe'] || [];
75
+ if (dedupe.length) GAME_EMPTY = false;
76
+ document.getElementById('lb-dedupe').innerHTML = dedupe.map(function(r) {
77
+ return '<tr><td title="' + sanitize(r.repo_root) + '">' + sanitize(r.repo_root.split('/').pop()) + '</td><td class="num">' + sanitize(String(r.value)) + '</td></tr>';
78
+ }).join('') || '<tr><td colspan="2" class="repo-none">no data</td></tr>';
79
+ var turns = results['turns'] || [];
80
+ var maxTurns = turns.reduce(function(mx, r) { return Math.max(mx, r.value); }, 0);
81
+ var lvl = Math.floor(Math.log2(maxTurns + 1)) + 1;
82
+ var lvlEl = document.getElementById('turns-level');
83
+ if (lvlEl) lvlEl.textContent = 'LVL ' + lvl;
84
+ if (turns.length) GAME_EMPTY = false;
85
+ // level-up pulse: when max level increases vs last poll, pulse the cache bar
86
+ if (lvl > lastMaxLevel && lastMaxLevel > 0) {
87
+ var bar = document.getElementById('ctx-bar');
88
+ if (bar) { bar.classList.add('level-up'); setTimeout(function() { bar.classList.remove('level-up'); }, 1200); }
89
+ }
90
+ if (lvl > lastMaxLevel) lastMaxLevel = lvl;
91
+ document.getElementById('lb-turns').innerHTML = turns.map(function(r) {
92
+ return '<tr><td title="' + sanitize(r.repo_root) + '">' + sanitize(r.repo_root.split('/').pop()) + '</td><td class="num">' + sanitize(String(r.value)) + '</td></tr>';
93
+ }).join('') || '<tr><td colspan="2" class="repo-none">no data</td></tr>';
94
+ // mega_cache trophies + banner + Opie tile + transient toast
95
+ var mega = results['mega_cache'] || [];
96
+ var megaBody = document.getElementById('lb-mega_cache');
97
+ if (megaBody) {
98
+ megaBody.innerHTML = mega.map(function(r) {
99
+ var m = trophyMeta(r.meta);
100
+ var fs = m.firstSeenTs || m.firstSeen || r.ts;
101
+ var extra = fs ? ' <span class="lb-meta">' + sanitize(fmtDate(fs)) + '</span>' : '';
102
+ 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>';
103
+ }).join('') || '<tr><td colspan="2" class="repo-none">no trophies yet</td></tr>';
104
+ }
105
+ var banner = document.getElementById('mega-cache-banner');
106
+ var tile = document.getElementById('opie-tile');
107
+ var best = null, firstSeen = null;
108
+ mega.forEach(function(r) {
109
+ if (best == null || r.value > best) best = r.value;
110
+ var m = trophyMeta(r.meta);
111
+ var fs = m.firstSeenTs || m.firstSeen || r.ts;
112
+ if (firstSeen == null || fs < firstSeen) firstSeen = fs;
113
+ });
114
+ if (banner) {
115
+ if (best != null && best > 100) {
116
+ banner.style.display = 'block';
117
+ banner.textContent = '🥧 MEGA CACHE! peak ' + fmtPct(best) + ' — first reached ' + fmtDate(firstSeen);
118
+ } else { banner.style.display = 'none'; }
119
+ }
120
+ if (tile) {
121
+ if (best != null && best > 100) {
122
+ tile.style.display = 'block';
123
+ tile.className = 'achievement-tile unlocked';
124
+ tile.innerHTML = '🏆 Opie\\'s Wild Ride<span class="ach-detail">best ' + sanitize(fmtPct(best)) + ' · first ' + sanitize(fmtDate(firstSeen)) + '</span>';
125
+ } else { tile.style.display = 'none'; tile.className = 'achievement-tile'; }
126
+ }
127
+ // transient oopsie toast: a NEW mega_cache trophy row since the last poll
128
+ var maxTs = mega.reduce(function(mx, r) { return Math.max(mx, r.ts); }, 0);
129
+ var newRow = mega.find(function(r) { return r.ts > lastMegaTs && r.value > 100; });
130
+ if (lastMegaTs && newRow) {
131
+ var toast = document.getElementById('mega-cache-toast');
132
+ if (toast) {
133
+ toast.textContent = 'oopsie! cache went to ' + Math.round(newRow.value) + '% — MEGA CACHE 🥧';
134
+ toast.classList.add('show');
135
+ setTimeout(function() { toast.classList.remove('show'); }, 4000);
136
+ }
137
+ }
138
+ if (maxTs > lastMegaTs) lastMegaTs = maxTs;
139
+ // empty state
140
+ var emptyEl = document.getElementById('game-empty');
141
+ if (emptyEl) emptyEl.style.display = GAME_EMPTY ? 'block' : 'none';
142
+ }
143
+ GAME_METRICS.forEach(function(m) {
144
+ 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);
145
+ });
146
+ fetch('/api/game-scores?metric=repos&limit=1').then(function(r) { return r.ok ? r.json() : []; }).then(function(rows) {
147
+ var badge = document.getElementById('repos-badge');
148
+ if (badge) badge.textContent = ((rows && rows.length) ? rows[0].value : 0) + ' repos';
149
+ if (rows && rows.length && rows[0].value > 0) GAME_EMPTY = false;
150
+ }).catch(function() {}).then(done);
151
+ }
152
+
153
+ // --- Achievements tile row (S35) ------------------------------------------
154
+ // Polls GET /api/achievements; renders the tile row (hidden+locked render
155
+ // NOTHING; unlocked show icon+title+date; visible-but-locked show ??? teaser)
156
+ // and fires a transient toast when a newly-unlocked achievement appears.
157
+ // Browser-side fetch only (PREVENT-PI-004).
158
+ var lastAchMaxTs = 0;
159
+ function renderAchievements() {
160
+ 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)
161
+ var box = document.getElementById('ach-tiles');
162
+ if (!box) return;
163
+ var html = '';
164
+ var maxTs = 0;
165
+ (rows || []).forEach(function(a) {
166
+ if (a.hidden === 1 && a.unlocked_at == null) return; // hidden invariant: render nothing
167
+ if (a.unlocked_at != null) {
168
+ var isNew = a.unlocked_at > lastAchMaxTs && lastAchMaxTs > 0;
169
+ maxTs = Math.max(maxTs, a.unlocked_at);
170
+ 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>';
171
+ } else {
172
+ html += '<div class="ach-tile locked">??? ' + sanitize(a.title) + '</div>';
173
+ }
174
+ });
175
+ box.innerHTML = html || '<span class="repo-none">no achievements yet</span>';
176
+ var newly = (rows || []).filter(function(a) { return a.unlocked_at != null && a.unlocked_at > lastAchMaxTs; });
177
+ if (lastAchMaxTs && newly.length) {
178
+ var toast = document.getElementById('ach-toast');
179
+ if (toast) {
180
+ toast.textContent = newly.map(function(a) { return (a.icon || '') + ' ' + a.title; }).join(', ') + ' unlocked!';
181
+ toast.classList.add('show');
182
+ setTimeout(function() { toast.classList.remove('show'); }, 4000);
183
+ }
184
+ }
185
+ if (maxTs > lastAchMaxTs) lastAchMaxTs = maxTs;
186
+ }).catch(function() {});
187
+ }
188
+
189
+ // --- Perf tab (v0.8.8) — live local instrumentation ----------------------
190
+ var perfPollTimer = null;
191
+ function pollPerf() {
192
+ 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)
193
+ if (!d) return;
194
+ var el = document.getElementById('perf-updated');
195
+ if (el) el.textContent = d.sampleCount + ' samples · updated ' + (d.updatedAt || '');
196
+ function setText(id, txt) { var e = document.getElementById(id); if (e) e.textContent = txt; }
197
+ function fmtMs(v) { return v == null ? '—' : (v >= 100 ? Math.round(v) + 'ms' : v.toFixed(1) + 'ms'); }
198
+ function fmtNum(v, dec) { return v == null ? '—' : (typeof v === 'number' ? v.toFixed(dec) : '—'); }
199
+ setText('pf-turn-p50', fmtMs(d.turn_latency_ms && d.turn_latency_ms.p50));
200
+ setText('pf-turn-p95', fmtMs(d.turn_latency_ms && d.turn_latency_ms.p95));
201
+ setText('pf-prov-p50', fmtMs(d.provider_latency_ms && d.provider_latency_ms.p50));
202
+ setText('pf-prov-p95', fmtMs(d.provider_latency_ms && d.provider_latency_ms.p95));
203
+ setText('pf-tps', fmtNum(d.tps && d.tps.avg, 1));
204
+ setText('pf-cache', (d.cache_hit_pct && typeof d.cache_hit_pct.avg === 'number') ? fmtNum(d.cache_hit_pct.avg, 1) + '%' : '—');
205
+ setText('pf-rss', (d.rss_mb && typeof d.rss_mb.latest === 'number') ? fmtNum(d.rss_mb.latest, 1) + ' MB' : '—');
206
+ setText('pf-heap', (d.heap_mb && typeof d.heap_mb.latest === 'number') ? fmtNum(d.heap_mb.latest, 1) + ' MB' : '—');
207
+ 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') : '—');
208
+ setText('pf-db-p50', fmtMs(d.db_recompute_ms && d.db_recompute_ms.p50));
209
+ setText('pf-db-p95', fmtMs(d.db_recompute_ms && d.db_recompute_ms.p95));
210
+ setText('pf-disk', fmtMs(d.disk_write_ms && d.disk_write_ms.p50));
211
+ var diag = d.diag || {};
212
+ setText('pf-recompute', diag.liveTrimFires != null ? String(diag.liveTrimFires) : '—');
213
+ setText('pf-replays', diag.liveTrimReplays != null ? String(diag.liveTrimReplays) : '—');
214
+ setText('pf-skips', diag.ctxFastGate != null ? String(diag.ctxFastGate) : '—');
215
+ }).catch(function() {});
216
+ }
217
+ function startPerfPoll() { if (perfPollTimer) return; pollPerf(); perfPollTimer = setInterval(pollPerf, 2000); }
218
+ function stopPerfPoll() { if (perfPollTimer) { clearInterval(perfPollTimer); perfPollTimer = null; } }
219
+
220
+ // --- Tab switching ------------------------------------------------------
221
+ var tabs = document.querySelectorAll('.tab');
222
+ var panels = { current: 'panel-current', all: 'panel-all', active: 'panel-active', summary: 'panel-summary', game: 'panel-game', perf: 'panel-perf' };
223
+ for (var i = 0; i < tabs.length; i++) {
224
+ tabs[i].addEventListener('click', function() {
225
+ var name = this.getAttribute('data-tab');
226
+ for (var j = 0; j < tabs.length; j++) tabs[j].classList.remove('active');
227
+ this.classList.add('active');
228
+ for (var k in panels) {
229
+ if (Object.prototype.hasOwnProperty.call(panels, k)) {
230
+ var el = document.getElementById(panels[k]);
231
+ if (el) el.classList.toggle('active', k === name);
232
+ }
233
+ }
234
+ if (name === 'all' || name === 'summary') pollIndex();
235
+ if (name === 'active') pollServers();
236
+ if (name === 'game') { renderGameScores(); renderAchievements(); }
237
+ if (name === 'perf') startPerfPoll(); else stopPerfPoll();
238
+ });
239
+ }
240
+ })();`;
241
+ }