pi-mega-compact 0.8.7 → 0.8.10

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 (110) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/core.js +7 -0
  2. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +104 -0
  3. package/dist/extensions/dashboard-server/api-contracts/game.js +9 -0
  4. package/dist/extensions/dashboard-server/api-contracts/index.js +9 -0
  5. package/dist/extensions/dashboard-server/api-contracts/infrastructure.js +10 -0
  6. package/dist/extensions/dashboard-server/api-contracts/multi-repo.js +9 -0
  7. package/dist/extensions/dashboard-server/api-contracts/snapshot.js +8 -0
  8. package/dist/extensions/dashboard-server/api-contracts.js +8 -0
  9. package/dist/extensions/dashboard-server/api-contracts.test.js +869 -0
  10. package/dist/extensions/dashboard-server/auth.js +18 -0
  11. package/dist/extensions/dashboard-server/helpers.js +37 -0
  12. package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
  13. package/dist/extensions/dashboard-server/html/body-open.js +23 -0
  14. package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
  15. package/dist/extensions/dashboard-server/html/head-open.js +16 -0
  16. package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
  17. package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
  18. package/dist/extensions/dashboard-server/html/script.js +259 -0
  19. package/dist/extensions/dashboard-server/html/styles.js +103 -0
  20. package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
  21. package/dist/extensions/dashboard-server/html-template.js +41 -0
  22. package/dist/extensions/dashboard-server/html.js +46 -1
  23. package/dist/extensions/dashboard-server/perf-server.test.js +80 -0
  24. package/dist/extensions/dashboard-server/server.js +269 -30
  25. package/dist/extensions/dashboard-server/tailscale.js +8 -0
  26. package/dist/extensions/dashboard-server/types.js +4 -0
  27. package/dist/extensions/mega-dashboard.js +9 -0
  28. package/dist/extensions/mega-events/perf-handler.js +71 -0
  29. package/dist/extensions/mega-events/register.js +2 -0
  30. package/dist/extensions/mega-events.js +1 -0
  31. package/dist/extensions/mega-runtime/state.js +59 -1
  32. package/dist/src/store/sqlite/connection.js +35 -0
  33. package/dist/src/store/sqlite/index-store.js +167 -0
  34. package/dist/src/store/sqlite/memory.js +54 -0
  35. package/dist/src/store/sqlite/minhash-lsh.js +47 -0
  36. package/dist/src/store/sqlite/perf-samples.js +81 -0
  37. package/dist/src/store/sqlite/perf-samples.test.js +54 -0
  38. package/dist/src/store/sqlite/schema.js +14 -0
  39. package/dist/src/store/sqlite/sessions.js +39 -0
  40. package/dist/src/store/sqlite/transaction.js +19 -0
  41. package/dist/src/store/sqlite.js +1 -0
  42. package/dist/src/vectorStore/add.js +260 -0
  43. package/dist/src/vectorStore/dedup.js +52 -0
  44. package/dist/src/vectorStore/index.js +10 -0
  45. package/dist/src/vectorStore/queries.js +83 -0
  46. package/dist/src/vectorStore/search.js +95 -0
  47. package/dist/src/vectorStore/session.js +19 -0
  48. package/dist/src/vectorStore/store.js +105 -0
  49. package/dist/src/vectorStore/types.js +6 -0
  50. package/dist/src/vectorStore/utils.js +23 -0
  51. package/extensions/dashboard-client/package-lock.json +1771 -0
  52. package/extensions/dashboard-client/package.json +27 -0
  53. package/extensions/dashboard-client/src/App.tsx +82 -0
  54. package/extensions/dashboard-client/src/api/client.ts +145 -0
  55. package/extensions/dashboard-client/src/components/CacheStatusPerModel.tsx +44 -0
  56. package/extensions/dashboard-client/src/components/CompressionCard.tsx +61 -0
  57. package/extensions/dashboard-client/src/components/ContextGauge.tsx +59 -0
  58. package/extensions/dashboard-client/src/components/DataSafetyCard.tsx +49 -0
  59. package/extensions/dashboard-client/src/components/ErrorBoundary.tsx +53 -0
  60. package/extensions/dashboard-client/src/components/EventCategoryFilter.tsx +16 -0
  61. package/extensions/dashboard-client/src/components/EventStream.tsx +152 -0
  62. package/extensions/dashboard-client/src/components/LoadingSpinner.tsx +7 -0
  63. package/extensions/dashboard-client/src/components/MemoryStatusCard.tsx +24 -0
  64. package/extensions/dashboard-client/src/components/ModelBadge.tsx +41 -0
  65. package/extensions/dashboard-client/src/components/PerfChart.tsx +160 -0
  66. package/extensions/dashboard-client/src/components/RepoDetailModal.tsx +126 -0
  67. package/extensions/dashboard-client/src/components/RepoTable.tsx +150 -0
  68. package/extensions/dashboard-client/src/components/SessionInfo.tsx +65 -0
  69. package/extensions/dashboard-client/src/components/SummaryTiles.tsx +52 -0
  70. package/extensions/dashboard-client/src/components/TabBar.tsx +34 -0
  71. package/extensions/dashboard-client/src/components/TriggerStatus.tsx +62 -0
  72. package/extensions/dashboard-client/src/hooks/useApi.ts +77 -0
  73. package/extensions/dashboard-client/src/hooks/useSSE.ts +87 -0
  74. package/extensions/dashboard-client/src/index.html +12 -0
  75. package/extensions/dashboard-client/src/main.tsx +23 -0
  76. package/extensions/dashboard-client/src/styles/base.css +121 -0
  77. package/extensions/dashboard-client/src/styles/overview-events.css +318 -0
  78. package/extensions/dashboard-client/src/styles/repos-metrics.css +307 -0
  79. package/extensions/dashboard-client/src/tabs/ConfigTab.tsx +16 -0
  80. package/extensions/dashboard-client/src/tabs/EventsTab.tsx +37 -0
  81. package/extensions/dashboard-client/src/tabs/MetricsTab.tsx +49 -0
  82. package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +80 -0
  83. package/extensions/dashboard-client/src/tabs/ReposTab.tsx +59 -0
  84. package/extensions/dashboard-client/tsconfig.json +28 -0
  85. package/extensions/dashboard-client/vite.config.ts +38 -0
  86. package/extensions/dashboard-server/api-contracts/core.ts +302 -0
  87. package/extensions/dashboard-server/api-contracts/endpoints.ts +476 -0
  88. package/extensions/dashboard-server/api-contracts/game.ts +145 -0
  89. package/extensions/dashboard-server/api-contracts/index.ts +159 -0
  90. package/extensions/dashboard-server/api-contracts/infrastructure.ts +465 -0
  91. package/extensions/dashboard-server/api-contracts/multi-repo.ts +328 -0
  92. package/extensions/dashboard-server/api-contracts/snapshot.ts +386 -0
  93. package/extensions/dashboard-server/api-contracts.test.ts +1050 -0
  94. package/extensions/dashboard-server/api-contracts.ts +96 -0
  95. package/extensions/dashboard-server/auth.ts +20 -0
  96. package/extensions/dashboard-server/html.ts +46 -1
  97. package/extensions/dashboard-server/perf-server.test.ts +101 -0
  98. package/extensions/dashboard-server/server.ts +862 -503
  99. package/extensions/dashboard-server/tailscale.ts +7 -0
  100. package/extensions/dashboard-server/types.ts +23 -114
  101. package/extensions/mega-dashboard.ts +17 -0
  102. package/extensions/mega-events/perf-handler.ts +113 -0
  103. package/extensions/mega-events/register.ts +2 -0
  104. package/extensions/mega-events.ts +1 -0
  105. package/extensions/mega-runtime/state.ts +57 -0
  106. package/package.json +2 -1
  107. package/src/store/sqlite/perf-samples.test.ts +65 -0
  108. package/src/store/sqlite/perf-samples.ts +125 -0
  109. package/src/store/sqlite/schema.ts +14 -0
  110. package/src/store/sqlite.ts +1 -0
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Inline CSS for the dashboard.
3
+ *
4
+ * Extracted verbatim from the original monolithic `html-template.ts`.
5
+ * Kept as a single cohesive stylesheet — splitting CSS into per-tab files
6
+ * would fragment the design system (shared tokens, e.g. `.card`, `.stat-grid`,
7
+ * `.meter-*` are reused across all tabs).
8
+ */
9
+ export function styles() {
10
+ return `<style>
11
+ * { margin: 0; padding: 0; box-sizing: border-box; }
12
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #0d1117; color: #c9d1d9; padding: 24px; line-height: 1.5; }
13
+ h1 { font-size: 20px; font-weight: 600; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; color: #f0f6fc; }
14
+ h1 .tier { background: #1f6feb; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .5px; }
15
+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px; }
16
+ .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
17
+ .card.safe { border-color: #238636; }
18
+ .card.safe h2 { color: #3fb950; }
19
+ .safe-note { font-size: 12px; color: #8b949e; margin: 12px 0 0; line-height: 1.5; }
20
+ .value.ok { color: #3fb950; }
21
+ .label {
22
+ cursor: help;
23
+ border-bottom: 1px dotted #484f58;
24
+ }
25
+ .card.legend { grid-column: 1 / -1; }
26
+ .legend-list { margin: 0; padding-left: 18px; color: #c9d1d9; }
27
+ .legend-list li { margin-bottom: 8px; line-height: 1.5; }
28
+ .legend-list b { color: #f0f6fc; }
29
+ .legend-note { font-size: 12px; color: #8b949e; margin: 12px 0 0; font-style: italic; }
30
+ .card h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 12px; font-weight: 600; }
31
+ .meter-track { background: #21262d; border-radius: 4px; height: 20px; overflow: hidden; margin: 8px 0; }
32
+ .meter-fill { height: 100%; border-radius: 4px; transition: width .6s ease; min-width: 2px; }
33
+ .meter-green { background: #238636; }
34
+ .meter-yellow { background: #d29922; }
35
+ .meter-red { background: #f85149; }
36
+ .meter-label { font-size: 24px; font-weight: 700; color: #f0f6fc; }
37
+ .meter-sub { font-size: 12px; color: #8b949e; }
38
+ .status-row { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-size: 14px; }
39
+ .status-row .bullet { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
40
+ .bullet-on { background: #3fb950; box-shadow: 0 0 6px #3fb95088; }
41
+ .bullet-off { background: #484f58; }
42
+ .bullet-na { background: #d29922; }
43
+ .state-text { font-size: 13px; color: #8b949e; margin-top: 8px; font-family: monospace; }
44
+ .stat-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 14px; }
45
+ .stat-grid .label { color: #8b949e; }
46
+ .stat-grid .value { color: #f0f6fc; font-weight: 600; font-family: monospace; }
47
+ .conf-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 14px; }
48
+ .conf-grid .label { color: #8b949e; }
49
+ .conf-grid .value { color: #f0f6fc; font-family: monospace; }
50
+ .events { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
51
+ .events h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 12px; font-weight: 600; }
52
+ .events-wrap { max-height: 240px; overflow-y: auto; font-family: monospace; font-size: 12px; }
53
+ .ev { padding: 3px 0; border-bottom: 1px solid #21262d; display: flex; gap: 8px; align-items: baseline; }
54
+ .ev:last-child { border-bottom: none; }
55
+ .ev-type { font-weight: 700; min-width: 70px; text-align: right; }
56
+ .ev-type-compact { color: #3fb950; }
57
+ .ev-type-recall { color: #a371f7; }
58
+ .ev-time { color: #484f58; font-size: 10px; min-width: 80px; }
59
+ .ev-detail { color: #8b949e; flex: 1; }
60
+ .updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
61
+ .empty { color: #484f58; font-style: italic; font-size: 13px; padding: 8px 0; }
62
+ .offline-banner { background: #f8514922; border: 1px solid #f85149; border-radius: 6px; padding: 10px 16px; margin-bottom: 16px; font-size: 13px; color: #f85149; display: none; }
63
+ .tabs { display: flex; gap: 8px; margin-bottom: 20px; }
64
+ .tab { background: #161b22; color: #8b949e; border: 1px solid #30363d; border-radius: 6px; padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all .15s ease; }
65
+ .tab:hover { color: #c9d1d9; border-color: #484f58; }
66
+ .tab.active { background: #1f6feb; color: #fff; border-color: #1f6feb; }
67
+ .tab.future { opacity: .7; }
68
+ .tab .soon { font-size: 9px; font-weight: 700; background: #6e40c9; color: #fff; padding: 1px 6px; border-radius: 8px; margin-left: 6px; text-transform: uppercase; letter-spacing: .5px; }
69
+ .tab-panel { display: none; }
70
+ .tab-panel.active { display: block; }
71
+ .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 20px; }
72
+ .summary-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
73
+ .summary-card .num { font-size: 24px; font-weight: 700; color: #f0f6fc; }
74
+ .summary-card .lbl { font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: .5px; margin-top: 4px; }
75
+ table.repos { width: 100%; border-collapse: collapse; background: #161b22; border: 1px solid #30363d; border-radius: 8px; overflow: hidden; }
76
+ table.repos th, table.repos td { text-align: left; padding: 10px 14px; font-size: 13px; border-bottom: 1px solid #21262d; }
77
+ table.repos th { color: #8b949e; text-transform: uppercase; letter-spacing: .5px; font-size: 11px; background: #0d1117; }
78
+ table.repos td.num { font-family: monospace; color: #f0f6fc; text-align: right; }
79
+ table.repos tr:last-child td { border-bottom: none; }
80
+ table.repos tr:hover td { background: #1c2128; }
81
+ .repo-model { color: #a371f7; }
82
+ .repo-none { color: #484f58; font-style: italic; }
83
+ .updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
84
+ .model-pill { background: #6e40c9; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .5px; }
85
+ .card.cost h2 { color: #a371f7; }
86
+ .cost-usd { font-size: 22px; font-weight: 700; color: #3fb950; }
87
+ .cost-sub { font-size: 12px; color: #8b949e; margin-top: 4px; }
88
+ .repo-link { cursor: pointer; }
89
+ .repo-link:hover td { color: #58a6ff; }
90
+ .repo-detail { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: none; align-items: center; justify-content: center; z-index: 50; }
91
+ .repo-detail.open { display: flex; }
92
+ .repo-detail-box { background: #161b22; border: 1px solid #30363d; border-radius: 10px; padding: 24px; width: 560px; max-width: 92vw; max-height: 86vh; overflow-y: auto; }
93
+ .repo-detail-box h2 { font-size: 14px; color: #f0f6fc; margin-bottom: 14px; display: flex; justify-content: space-between; align-items: center; }
94
+ .repo-close { cursor: pointer; color: #8b949e; font-size: 20px; line-height: 1; border: none; background: none; padding: 0 4px; }
95
+ .repo-close:hover { color: #f0f6fc; }
96
+ .repo-path { font-size: 11px; color: #484f58; word-break: break-all; margin: -8px 0 12px; }
97
+ .placeholder { text-align: center; padding: 48px 16px; color: #8b949e; }
98
+ .placeholder .em { font-size: 40px; margin-bottom: 12px; }
99
+ .placeholder h2 { font-size: 16px; color: #f0f6fc; margin-bottom: 8px; }
100
+ .placeholder p { font-size: 13px; line-height: 1.6; max-width: 420px; margin: 0 auto; }
101
+ </style>
102
+ `;
103
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * "Summary" tab panel — aggregate counts across all registered repos.
3
+ *
4
+ * Populated by the index poller in `script.ts`.
5
+ */
6
+ export function summaryTab() {
7
+ return `<!-- Summary (aggregate across all repos) -->
8
+ <div class="tab-panel" id="panel-summary">
9
+ <div class="summary-grid">
10
+ <div class="summary-card"><div class="num" id="sm-repos">0</div><div class="lbl">Repositories</div></div>
11
+ <div class="summary-card"><div class="num" id="sm-checkpoints">0</div><div class="lbl">Total Checkpoints</div></div>
12
+ <div class="summary-card"><div class="num" id="sm-saved">0</div><div class="lbl">Total Tokens Saved</div></div>
13
+ <div class="summary-card"><div class="num" id="sm-bytes">0 B</div><div class="lbl">Compressed-Original</div></div>
14
+ </div>
15
+ <div class="updated" id="sm-updated"></div>
16
+ </div>
17
+
18
+ `;
19
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * HTML template for the dashboard server — composer.
3
+ *
4
+ * Originally a single 518-line monolithic file returning one large HTML page
5
+ * template. Split into per-concern fragments under `./html/` (each tab is its
6
+ * own file; plus shared styles, header chrome, the repo-detail modal, and the
7
+ * client script). This file composes them back into the full document.
8
+ *
9
+ * `tierName` is threaded into the heading badge and the Configuration card.
10
+ *
11
+ * Fragment layout:
12
+ * headOpen() — <!DOCTYPE html>…<title>
13
+ * styles() — <style>…</style> (shared CSS, design-system level)
14
+ * bodyOpen(tier) — </head><body>…<h1>…</nav> (incl. High Score future tab)
15
+ * currentRepoTab(tier) — panel-current (cards + event stream + in-panel table)
16
+ * allReposTab() — panel-all (machine-wide repo table)
17
+ * summaryTab() — panel-summary (aggregate cards)
18
+ * highScoreTab() — panel-highscore (FUTURE placeholder, next milestone)
19
+ * repoDetailModal() — per-repo overlay (position-independent)
20
+ * script() — <script>…</script></body></html>
21
+ */
22
+ import { headOpen } from "./html/head-open.js";
23
+ import { styles } from "./html/styles.js";
24
+ import { bodyOpen } from "./html/body-open.js";
25
+ import { currentRepoTab } from "./html/current-repo-tab.js";
26
+ import { allReposTab } from "./html/all-repos-tab.js";
27
+ import { summaryTab } from "./html/summary-tab.js";
28
+ import { highScoreTab } from "./html/high-score-tab.js";
29
+ import { repoDetailModal } from "./html/repo-detail-modal.js";
30
+ import { script } from "./html/script.js";
31
+ export function dashboardHtml(tierName) {
32
+ return (headOpen() +
33
+ styles() +
34
+ bodyOpen(tierName) +
35
+ currentRepoTab(tierName) +
36
+ repoDetailModal() +
37
+ allReposTab() +
38
+ summaryTab() +
39
+ highScoreTab() +
40
+ script());
41
+ }
@@ -186,6 +186,7 @@ export function dashboardHtml(tierName) {
186
186
  <button class="tab" data-tab="active">Active Repos</button>
187
187
  <button class="tab" data-tab="summary">Summary</button>
188
188
  <button class="tab" data-tab="game">Game Mode</button>
189
+ <button class="tab" data-tab="perf">Perf</button>
189
190
  </nav>
190
191
 
191
192
  <!-- Current repo (existing single-repo view) -->
@@ -440,6 +441,18 @@ export function dashboardHtml(tierName) {
440
441
  <div id="game-empty">No scores yet — run a session with game mode on.</div>
441
442
  </div>
442
443
 
444
+ <!-- Perf (v0.8.8) — live local instrumentation -->
445
+ <div class="tab-panel" id="panel-perf">
446
+ <div class="grid">
447
+ <div class="card"><h2>Model latency</h2><div class="stat-grid"><span class="label">Turn p50</span><span class="value" id="pf-turn-p50">—</span><span class="label">Turn p95</span><span class="value" id="pf-turn-p95">—</span><span class="label">Provider p50</span><span class="value" id="pf-prov-p50">—</span><span class="label">Provider p95</span><span class="value" id="pf-prov-p95">—</span></div></div>
448
+ <div class="card"><h2>Throughput</h2><div class="stat-grid"><span class="label">TPS (avg)</span><span class="value" id="pf-tps">—</span><span class="label">Cache hit %</span><span class="value" id="pf-cache">—</span></div></div>
449
+ <div class="card"><h2>Process</h2><div class="stat-grid"><span class="label">RSS</span><span class="value" id="pf-rss">—</span><span class="label">Heap</span><span class="value" id="pf-heap">—</span><span class="label">CPU user/sys</span><span class="value" id="pf-cpu">—</span></div></div>
450
+ <div class="card"><h2>Snapshot cost</h2><div class="stat-grid"><span class="label">DB recompute p50</span><span class="value" id="pf-db-p50">—</span><span class="label">DB recompute p95</span><span class="value" id="pf-db-p95">—</span><span class="label">Disk write p50</span><span class="value" id="pf-disk">—</span></div></div>
451
+ <div class="card"><h2>TUI lag proxy</h2><div class="stat-grid"><span class="label">Live-trim fires</span><span class="value" id="pf-recompute">—</span><span class="label">Cache replays</span><span class="value" id="pf-replays">—</span><span class="label">Fast-gate skips</span><span class="value" id="pf-skips">—</span></div><div class="meter-sub">skip vs recompute vs replay cadence</div></div>
452
+ </div>
453
+ <div class="updated" id="perf-updated">waiting for data</div>
454
+ </div>
455
+
443
456
  <script>
444
457
  (function() {
445
458
  var evBox = document.getElementById('events');
@@ -997,9 +1010,40 @@ export function dashboardHtml(tierName) {
997
1010
  }).catch(function() {});
998
1011
  }
999
1012
 
1013
+ // --- Perf tab (v0.8.8) — live local instrumentation ----------------------
1014
+ var perfPollTimer = null;
1015
+ function pollPerf() {
1016
+ 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)
1017
+ if (!d) return;
1018
+ var el = document.getElementById('perf-updated');
1019
+ if (el) el.textContent = d.sampleCount + ' samples \u00b7 updated ' + (d.updatedAt || '');
1020
+ function setText(id, txt) { var e = document.getElementById(id); if (e) e.textContent = txt; }
1021
+ function fmtMs(v) { return v == null ? '\u2014' : (v >= 100 ? Math.round(v) + 'ms' : v.toFixed(1) + 'ms'); }
1022
+ function fmtNum(v, dec) { return v == null ? '\u2014' : (typeof v === 'number' ? v.toFixed(dec) : '\u2014'); }
1023
+ setText('pf-turn-p50', fmtMs(d.turn_latency_ms && d.turn_latency_ms.p50));
1024
+ setText('pf-turn-p95', fmtMs(d.turn_latency_ms && d.turn_latency_ms.p95));
1025
+ setText('pf-prov-p50', fmtMs(d.provider_latency_ms && d.provider_latency_ms.p50));
1026
+ setText('pf-prov-p95', fmtMs(d.provider_latency_ms && d.provider_latency_ms.p95));
1027
+ setText('pf-tps', fmtNum(d.tps && d.tps.avg, 1));
1028
+ setText('pf-cache', (d.cache_hit_pct && typeof d.cache_hit_pct.avg === 'number') ? fmtNum(d.cache_hit_pct.avg, 1) + '%' : '\u2014');
1029
+ setText('pf-rss', (d.rss_mb && typeof d.rss_mb.latest === 'number') ? fmtNum(d.rss_mb.latest, 1) + ' MB' : '\u2014');
1030
+ setText('pf-heap', (d.heap_mb && typeof d.heap_mb.latest === 'number') ? fmtNum(d.heap_mb.latest, 1) + ' MB' : '\u2014');
1031
+ 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');
1032
+ setText('pf-db-p50', fmtMs(d.db_recompute_ms && d.db_recompute_ms.p50));
1033
+ setText('pf-db-p95', fmtMs(d.db_recompute_ms && d.db_recompute_ms.p95));
1034
+ setText('pf-disk', fmtMs(d.disk_write_ms && d.disk_write_ms.p50));
1035
+ var diag = d.diag || {};
1036
+ setText('pf-recompute', diag.liveTrimFires != null ? String(diag.liveTrimFires) : '\u2014');
1037
+ setText('pf-replays', diag.liveTrimReplays != null ? String(diag.liveTrimReplays) : '\u2014');
1038
+ setText('pf-skips', diag.ctxFastGate != null ? String(diag.ctxFastGate) : '\u2014');
1039
+ }).catch(function() {});
1040
+ }
1041
+ function startPerfPoll() { if (perfPollTimer) return; pollPerf(); perfPollTimer = setInterval(pollPerf, 2000); }
1042
+ function stopPerfPoll() { if (perfPollTimer) { clearInterval(perfPollTimer); perfPollTimer = null; } }
1043
+
1000
1044
  // --- Tab switching ------------------------------------------------------
1001
1045
  var tabs = document.querySelectorAll('.tab');
1002
- var panels = { current: 'panel-current', all: 'panel-all', active: 'panel-active', summary: 'panel-summary', game: 'panel-game' };
1046
+ var panels = { current: 'panel-current', all: 'panel-all', active: 'panel-active', summary: 'panel-summary', game: 'panel-game', perf: 'panel-perf' };
1003
1047
  for (var i = 0; i < tabs.length; i++) {
1004
1048
  tabs[i].addEventListener('click', function() {
1005
1049
  var name = this.getAttribute('data-tab');
@@ -1014,6 +1058,7 @@ export function dashboardHtml(tierName) {
1014
1058
  if (name === 'all' || name === 'summary') pollIndex();
1015
1059
  if (name === 'active') pollServers();
1016
1060
  if (name === 'game') { renderGameScores(); renderAchievements(); }
1061
+ if (name === 'perf') startPerfPoll(); else stopPerfPoll();
1017
1062
  });
1018
1063
  }
1019
1064
  })();
@@ -0,0 +1,80 @@
1
+ /**
2
+ * perf-server.test.ts — v0.8.8 /api/perf endpoint (GET aggregates + 405).
3
+ * Mirrors the server.test.ts spawn-and-fetch harness (self-contained so the
4
+ * dashboard HTTP-port lane stays isolated).
5
+ */
6
+ import { test, describe } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import { mkdtempSync, rmSync, readFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { spawn } from "node:child_process";
12
+ import { recordPerfSample } from "../../src/store/sqlite.js";
13
+ const SERVER_ENTRY = new URL("./server.js", import.meta.url).pathname;
14
+ function waitFor(cond, timeoutMs = 6000) {
15
+ const start = Date.now();
16
+ return new Promise((resolve, reject) => {
17
+ const tick = async () => {
18
+ if (await cond())
19
+ return resolve();
20
+ if (Date.now() - start > timeoutMs)
21
+ return reject(new Error("timeout"));
22
+ setTimeout(tick, 50);
23
+ };
24
+ tick();
25
+ });
26
+ }
27
+ function freshDir(prefix) {
28
+ return mkdtempSync(join(tmpdir(), prefix));
29
+ }
30
+ async function withServer(port, dir, fn) {
31
+ process.env.MEGACOMPACT_DASHBOARD_PORT = port;
32
+ const child = spawn(process.execPath, [SERVER_ENTRY, dir], { stdio: "ignore" });
33
+ try {
34
+ await waitFor(async () => {
35
+ try {
36
+ const raw = JSON.parse(readFileSync(join(dir, "port.pid"), "utf-8"));
37
+ const res = await fetch(`http://localhost:${raw.port}/api/version`);
38
+ return res.ok;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ });
44
+ const raw = JSON.parse(readFileSync(join(dir, "port.pid"), "utf-8"));
45
+ return await fn(raw.port);
46
+ }
47
+ finally {
48
+ child.kill("SIGTERM");
49
+ delete process.env.MEGACOMPACT_DASHBOARD_PORT;
50
+ rmSync(dir, { recursive: true, force: true });
51
+ }
52
+ }
53
+ describe("v0.8.8 /api/perf", () => {
54
+ test("GET returns aggregates over recorded perf_samples", async () => {
55
+ const dir = freshDir("dash-perf-agg-");
56
+ recordPerfSample(dir, "turn_latency_ms", 100, { turnIndex: 1 });
57
+ recordPerfSample(dir, "turn_latency_ms", 200);
58
+ recordPerfSample(dir, "tps", 50);
59
+ recordPerfSample(dir, "rss_mb", 256);
60
+ await withServer("19440", dir, async (port) => {
61
+ const res = await fetch(`http://localhost:${port}/api/perf?minutes=30`);
62
+ assert.equal(res.status, 200);
63
+ const d = (await res.json());
64
+ assert.equal(d.sampleCount, 4);
65
+ assert.equal(d.turn_latency_ms.n, 2);
66
+ assert.equal(d.turn_latency_ms.p50, 100); // nearest-rank p50 of [100,200]
67
+ assert.equal(d.turn_latency_ms.p95, 200); // nearest-rank p95 of [100,200]
68
+ assert.equal(d.tps.avg, 50);
69
+ assert.equal(d.rss_mb.latest, 256);
70
+ assert.equal(d.diag, null); // no runtime wrote dashboard.json in this dir
71
+ });
72
+ });
73
+ test("non-GET (POST) -> 405", async () => {
74
+ const dir = freshDir("dash-perf-meth-");
75
+ await withServer("19441", dir, async (port) => {
76
+ const res = await fetch(`http://localhost:${port}/api/perf`, { method: "POST" });
77
+ assert.equal(res.status, 405);
78
+ });
79
+ });
80
+ });