pi-mega-compact 0.7.9 โ†’ 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/extensions/dashboard-server/html.js +333 -66
  2. package/dist/extensions/dashboard-server/html.test.js +41 -0
  3. package/dist/extensions/dashboard-server/server.js +161 -1
  4. package/dist/extensions/dashboard-server/server.test.js +120 -0
  5. package/dist/extensions/dashboard-server-s32.test.js +181 -0
  6. package/dist/extensions/mega-compact.js +3 -0
  7. package/dist/extensions/mega-events/agent-handlers.js +49 -0
  8. package/dist/extensions/mega-events/compact-handlers.js +29 -0
  9. package/dist/extensions/mega-game-cmds.js +106 -0
  10. package/dist/extensions/mega-game-cmds.test.js +113 -0
  11. package/dist/extensions/mega-runtime/state.js +167 -1
  12. package/dist/extensions/mega-runtime/state.test.js +171 -0
  13. package/dist/extensions/mega-runtime/widget.js +93 -20
  14. package/dist/extensions/mega-runtime/widget.test.js +160 -0
  15. package/dist/src/config/themes.js +84 -0
  16. package/dist/src/config/themes.test.js +94 -0
  17. package/dist/src/game/scoring.js +105 -0
  18. package/dist/src/game/scoring.test.js +98 -0
  19. package/dist/src/store/sqlite/game-achievements.js +111 -0
  20. package/dist/src/store/sqlite/game-achievements.test.js +67 -0
  21. package/dist/src/store/sqlite/game-scores.js +105 -0
  22. package/dist/src/store/sqlite/game-scores.test.js +106 -0
  23. package/dist/src/store/sqlite/game-state.js +54 -0
  24. package/dist/src/store/sqlite/game-state.test.js +76 -0
  25. package/dist/src/store/sqlite/schema.js +44 -0
  26. package/dist/src/store/sqlite.js +3 -0
  27. package/extensions/dashboard-server/html.test.ts +50 -0
  28. package/extensions/dashboard-server/html.ts +334 -66
  29. package/extensions/dashboard-server/server.test.ts +131 -0
  30. package/extensions/dashboard-server/server.ts +148 -1
  31. package/extensions/dashboard-server-s32.test.ts +195 -0
  32. package/extensions/mega-compact.ts +3 -0
  33. package/extensions/mega-events/agent-handlers.ts +48 -0
  34. package/extensions/mega-events/compact-handlers.ts +28 -0
  35. package/extensions/mega-game-cmds.test.ts +137 -0
  36. package/extensions/mega-game-cmds.ts +122 -0
  37. package/extensions/mega-runtime/state.test.ts +171 -0
  38. package/extensions/mega-runtime/state.ts +170 -0
  39. package/extensions/mega-runtime/widget.test.ts +185 -0
  40. package/extensions/mega-runtime/widget.ts +119 -18
  41. package/package.json +1 -1
  42. package/src/config/themes.test.ts +116 -0
  43. package/src/config/themes.ts +124 -0
  44. package/src/game/scoring.test.ts +103 -0
  45. package/src/game/scoring.ts +158 -0
  46. package/src/store/sqlite/game-achievements.test.ts +80 -0
  47. package/src/store/sqlite/game-achievements.ts +147 -0
  48. package/src/store/sqlite/game-scores.test.ts +132 -0
  49. package/src/store/sqlite/game-scores.ts +168 -0
  50. package/src/store/sqlite/game-state.test.ts +89 -0
  51. package/src/store/sqlite/game-state.ts +87 -0
  52. package/src/store/sqlite/schema.ts +47 -0
  53. package/src/store/sqlite.ts +3 -0
  54. package/dist/extensions/dashboard-server/helpers.js +0 -37
  55. package/dist/extensions/dashboard-server/html/all-repos-tab.js +0 -26
  56. package/dist/extensions/dashboard-server/html/body-open.js +0 -23
  57. package/dist/extensions/dashboard-server/html/current-repo-tab.js +0 -130
  58. package/dist/extensions/dashboard-server/html/head-open.js +0 -16
  59. package/dist/extensions/dashboard-server/html/high-score-tab.js +0 -25
  60. package/dist/extensions/dashboard-server/html/repo-detail-modal.js +0 -26
  61. package/dist/extensions/dashboard-server/html/script.js +0 -259
  62. package/dist/extensions/dashboard-server/html/styles.js +0 -103
  63. package/dist/extensions/dashboard-server/html/summary-tab.js +0 -19
  64. package/dist/extensions/dashboard-server/html-template.js +0 -41
  65. package/dist/src/store/sqlite/connection.js +0 -35
  66. package/dist/src/store/sqlite/index-store.js +0 -167
  67. package/dist/src/store/sqlite/memory.js +0 -54
  68. package/dist/src/store/sqlite/minhash-lsh.js +0 -47
  69. package/dist/src/store/sqlite/sessions.js +0 -39
  70. package/dist/src/store/sqlite/transaction.js +0 -19
  71. package/dist/src/vectorStore/add.js +0 -260
  72. package/dist/src/vectorStore/dedup.js +0 -52
  73. package/dist/src/vectorStore/index.js +0 -10
  74. package/dist/src/vectorStore/queries.js +0 -83
  75. package/dist/src/vectorStore/search.js +0 -95
  76. package/dist/src/vectorStore/session.js +0 -19
  77. package/dist/src/vectorStore/store.js +0 -105
  78. package/dist/src/vectorStore/types.js +0 -6
  79. package/dist/src/vectorStore/utils.js +0 -23
@@ -2,99 +2,170 @@
2
2
  * dashboard-server/html.ts โ€” single-page HTML dashboard template.
3
3
  */
4
4
  import { dashboardServerVersion } from "./state.js";
5
+ import { THEMES, DEFAULT_THEME, themeDataBlock } from "../../src/config/themes.js";
6
+ // Server-side injection of every theme's :root[data-theme="<id>"] CSS-var
7
+ // override block so the client can switch themes instantly by setting
8
+ // document.documentElement.dataset.theme. PREVENT-PI-004: pure local, no network.
9
+ const THEME_STYLE_BLOCKS = THEMES.map(themeDataBlock).join("\n");
10
+ const THEME_OPTIONS = THEMES.map((t) => `<option value="${t.id}">${t.label}</option>`).join("");
5
11
  export function dashboardHtml(tierName) {
6
12
  return `<!DOCTYPE html>
7
- <html lang="en">
13
+ <html lang="en" data-theme="${DEFAULT_THEME}">
8
14
  <head>
9
15
  <meta charset="utf-8">
10
16
  <meta name="viewport" content="width=device-width, initial-scale=1">
11
17
  <title>mega-compact dashboard</title>
12
18
  <style>
19
+ /* S32: CSS-variable skin. Base :root holds the CURRENT hardcoded hexes so the
20
+ default (data-theme="transparent") is visually identical to the pre-S32
21
+ dashboard. Each :root[data-theme="<id>"] block (injected below from
22
+ src/config/themes.ts) overrides the 4 theme vars (--bg/--fg/--accent/--mega).
23
+ Non-theme palette tokens (card bg, borders, muted text, meter colors) stay
24
+ fixed so visual parity is preserved under the transparent default. */
25
+ :root {
26
+ --bg: #0d1117; /* page background (transparent theme -> transparent) */
27
+ --fg: #c9d1d9; /* default foreground */
28
+ --accent: #3fb950; /* accent (bars, ok values, on-bullets) */
29
+ --mega: #f0883e; /* MEGA CACHE highlight */
30
+ --fg-strong: #f0f6fc; /* headings, strong values */
31
+ --muted: #8b949e; /* labels, sub-text */
32
+ --dim: #484f58; /* timestamps, empty states */
33
+ --card-bg: #161b22; /* card / events / table background */
34
+ --border: #30363d; /* card / table borders */
35
+ --border-soft: #21262d; /* meter track, ev borders, table row borders */
36
+ --blue: #1f6feb; /* tier pill, active tab */
37
+ --green-bar: #238636; /* green meter fill, safe border */
38
+ --yellow-bar: #d29922; /* yellow meter fill, na-bullet */
39
+ --red-bar: #f85149; /* red meter fill, offline banner */
40
+ --purple: #a371f7; /* recall events, repo model, cost h2 */
41
+ --purple-pill: #6e40c9; /* model pill bg */
42
+ --hover-row: #1c2128; /* table row hover */
43
+ --link: #58a6ff; /* repo-link hover */
44
+ --th-bg: #0d1117; /* table header background */
45
+ }
46
+ /* html backdrop keeps the page dark even when --bg is transparent (transparent
47
+ theme) so there's no white flash โ€” visually identical to the pre-S32 fill. */
48
+ html { background: #0d1117; }
49
+ ${THEME_STYLE_BLOCKS}
13
50
  * { margin: 0; padding: 0; box-sizing: border-box; }
14
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #0d1117; color: #c9d1d9; padding: 24px; line-height: 1.5; }
15
- h1 { font-size: 20px; font-weight: 600; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; color: #f0f6fc; }
16
- h1 .tier { background: #1f6feb; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .5px; }
17
- h1 .version-pill { background: #30363d; color: #8b949e; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
51
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: var(--bg); color: var(--fg); padding: 24px; line-height: 1.5; }
52
+ h1 { font-size: 20px; font-weight: 600; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; color: var(--fg-strong); }
53
+ h1 .tier { background: var(--blue); color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .5px; }
54
+ h1 .version-pill { background: var(--border); color: var(--muted); font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
18
55
  .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px; }
19
- .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
20
- .card.safe { border-color: #238636; }
21
- .card.safe h2 { color: #3fb950; }
22
- .safe-note { font-size: 12px; color: #8b949e; margin: 12px 0 0; line-height: 1.5; }
23
- .value.ok { color: #3fb950; }
56
+ .card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
57
+ .card.safe { border-color: var(--green-bar); }
58
+ .card.safe h2 { color: var(--accent); }
59
+ .safe-note { font-size: 12px; color: var(--muted); margin: 12px 0 0; line-height: 1.5; }
60
+ .value.ok { color: var(--accent); }
24
61
  .label {
25
62
  cursor: help;
26
- border-bottom: 1px dotted #484f58;
63
+ border-bottom: 1px dotted var(--dim);
27
64
  }
28
65
  .card.legend { grid-column: 1 / -1; }
29
- .legend-list { margin: 0; padding-left: 18px; color: #c9d1d9; }
66
+ .legend-list { margin: 0; padding-left: 18px; color: var(--fg); }
30
67
  .legend-list li { margin-bottom: 8px; line-height: 1.5; }
31
- .legend-list b { color: #f0f6fc; }
32
- .legend-note { font-size: 12px; color: #8b949e; margin: 12px 0 0; font-style: italic; }
33
- .card h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 12px; font-weight: 600; }
34
- .meter-track { background: #21262d; border-radius: 4px; height: 20px; overflow: hidden; margin: 8px 0; }
68
+ .legend-list b { color: var(--fg-strong); }
69
+ .legend-note { font-size: 12px; color: var(--muted); margin: 12px 0 0; font-style: italic; }
70
+ .card h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); margin-bottom: 12px; font-weight: 600; }
71
+ .meter-track { background: var(--border-soft); border-radius: 4px; height: 20px; overflow: hidden; margin: 8px 0; }
35
72
  .meter-fill { height: 100%; border-radius: 4px; transition: width .6s ease; min-width: 2px; }
36
- .meter-green { background: #238636; }
37
- .meter-yellow { background: #d29922; }
38
- .meter-red { background: #f85149; }
39
- .meter-label { font-size: 24px; font-weight: 700; color: #f0f6fc; }
40
- .meter-sub { font-size: 12px; color: #8b949e; }
73
+ .meter-green { background: var(--green-bar); }
74
+ .meter-yellow { background: var(--yellow-bar); }
75
+ .meter-red { background: var(--red-bar); }
76
+ .meter-label { font-size: 24px; font-weight: 700; color: var(--fg-strong); }
77
+ .meter-sub { font-size: 12px; color: var(--muted); }
41
78
  .status-row { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-size: 14px; }
42
79
  .status-row .bullet { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
43
- .bullet-on { background: #3fb950; box-shadow: 0 0 6px #3fb95088; }
44
- .bullet-off { background: #484f58; }
45
- .bullet-na { background: #d29922; }
46
- .state-text { font-size: 13px; color: #8b949e; margin-top: 8px; font-family: monospace; }
80
+ .bullet-on { background: var(--accent); box-shadow: 0 0 6px #3fb95088; }
81
+ .bullet-off { background: var(--dim); }
82
+ .bullet-na { background: var(--yellow-bar); }
83
+ .state-text { font-size: 13px; color: var(--muted); margin-top: 8px; font-family: monospace; }
47
84
  .stat-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 14px; }
48
- .stat-grid .label { color: #8b949e; }
49
- .stat-grid .value { color: #f0f6fc; font-weight: 600; font-family: monospace; }
85
+ .stat-grid .label { color: var(--muted); }
86
+ .stat-grid .value { color: var(--fg-strong); font-weight: 600; font-family: monospace; }
50
87
  .conf-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 14px; }
51
- .conf-grid .label { color: #8b949e; }
52
- .conf-grid .value { color: #f0f6fc; font-family: monospace; }
53
- .events { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
54
- .events h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 12px; font-weight: 600; }
88
+ .conf-grid .label { color: var(--muted); }
89
+ .conf-grid .value { color: var(--fg-strong); font-family: monospace; }
90
+ .events { background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
91
+ .events h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); margin-bottom: 12px; font-weight: 600; }
55
92
  .events-wrap { max-height: 240px; overflow-y: auto; font-family: monospace; font-size: 12px; }
56
- .ev { padding: 3px 0; border-bottom: 1px solid #21262d; display: flex; gap: 8px; align-items: baseline; }
93
+ .ev { padding: 3px 0; border-bottom: 1px solid var(--border-soft); display: flex; gap: 8px; align-items: baseline; }
57
94
  .ev:last-child { border-bottom: none; }
58
95
  .ev-type { font-weight: 700; min-width: 70px; text-align: right; }
59
- .ev-type-compact { color: #3fb950; }
60
- .ev-type-recall { color: #a371f7; }
61
- .ev-time { color: #484f58; font-size: 10px; min-width: 80px; }
62
- .ev-detail { color: #8b949e; flex: 1; }
63
- .updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
64
- .empty { color: #484f58; font-style: italic; font-size: 13px; padding: 8px 0; }
65
- .offline-banner { background: #f8514922; border: 1px solid #f85149; border-radius: 6px; padding: 10px 16px; margin-bottom: 16px; font-size: 13px; color: #f85149; display: none; }
96
+ .ev-type-compact { color: var(--accent); }
97
+ .ev-type-recall { color: var(--purple); }
98
+ .ev-time { color: var(--dim); font-size: 10px; min-width: 80px; }
99
+ .ev-detail { color: var(--muted); flex: 1; }
100
+ .updated { font-size: 11px; color: var(--dim); margin-top: 16px; text-align: right; }
101
+ .empty { color: var(--dim); font-style: italic; font-size: 13px; padding: 8px 0; }
102
+ .offline-banner { background: #f8514922; border: 1px solid var(--red-bar); border-radius: 6px; padding: 10px 16px; margin-bottom: 16px; font-size: 13px; color: var(--red-bar); display: none; }
66
103
  .tabs { display: flex; gap: 8px; margin-bottom: 20px; }
67
- .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; }
68
- .tab:hover { color: #c9d1d9; border-color: #484f58; }
69
- .tab.active { background: #1f6feb; color: #fff; border-color: #1f6feb; }
104
+ .tab { background: var(--card-bg); color: var(--muted); border: 1px solid var(--border); border-radius: 6px; padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all .15s ease; }
105
+ .tab:hover { color: var(--fg); border-color: var(--dim); }
106
+ .tab.active { background: var(--blue); color: #fff; border-color: var(--blue); }
70
107
  .tab-panel { display: none; }
71
108
  .tab-panel.active { display: block; }
109
+ /* S32: header settings strip (game-mode toggle + theme + TUI display-mode) */
110
+ .settings-strip { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; padding: 10px 14px; background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; font-size: 13px; color: var(--muted); flex-wrap: wrap; }
111
+ .settings-strip label { display: flex; align-items: center; gap: 6px; font-weight: 600; }
112
+ .settings-strip input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; accent-color: var(--accent); }
113
+ .settings-strip select { background: var(--th-bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; padding: 4px 8px; font-size: 13px; cursor: pointer; }
114
+ .settings-strip select:hover { border-color: var(--dim); }
72
115
  .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 20px; }
73
- .summary-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
74
- .summary-card .num { font-size: 24px; font-weight: 700; color: #f0f6fc; }
75
- .summary-card .lbl { font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: .5px; margin-top: 4px; }
76
- table.repos { width: 100%; border-collapse: collapse; background: #161b22; border: 1px solid #30363d; border-radius: 8px; overflow: hidden; }
77
- table.repos th, table.repos td { text-align: left; padding: 10px 14px; font-size: 13px; border-bottom: 1px solid #21262d; }
78
- table.repos th { color: #8b949e; text-transform: uppercase; letter-spacing: .5px; font-size: 11px; background: #0d1117; }
79
- table.repos td.num { font-family: monospace; color: #f0f6fc; text-align: right; }
116
+ .summary-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
117
+ .summary-card .num { font-size: 24px; font-weight: 700; color: var(--fg-strong); }
118
+ .summary-card .lbl { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; margin-top: 4px; }
119
+ table.repos { width: 100%; border-collapse: collapse; background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
120
+ table.repos th, table.repos td { text-align: left; padding: 10px 14px; font-size: 13px; border-bottom: 1px solid var(--border-soft); }
121
+ table.repos th { color: var(--muted); text-transform: uppercase; letter-spacing: .5px; font-size: 11px; background: var(--th-bg); }
122
+ table.repos td.num { font-family: monospace; color: var(--fg-strong); text-align: right; }
80
123
  table.repos tr:last-child td { border-bottom: none; }
81
- table.repos tr:hover td { background: #1c2128; }
82
- .repo-model { color: #a371f7; }
83
- .repo-none { color: #484f58; font-style: italic; }
84
- .updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
85
- .model-pill { background: #6e40c9; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .5px; }
86
- .card.cost h2 { color: #a371f7; }
87
- .cost-usd { font-size: 22px; font-weight: 700; color: #3fb950; }
88
- .cost-sub { font-size: 12px; color: #8b949e; margin-top: 4px; }
124
+ table.repos tr:hover td { background: var(--hover-row); }
125
+ .repo-model { color: var(--purple); }
126
+ .repo-none { color: var(--dim); font-style: italic; }
127
+ .updated { font-size: 11px; color: var(--dim); margin-top: 16px; text-align: right; }
128
+ .model-pill { background: var(--purple-pill); color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .5px; }
129
+ .card.cost h2 { color: var(--purple); }
130
+ .cost-usd { font-size: 22px; font-weight: 700; color: var(--accent); }
131
+ .cost-sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
89
132
  .repo-link { cursor: pointer; }
90
- .repo-link:hover td { color: #58a6ff; }
133
+ .repo-link:hover td { color: var(--link); }
91
134
  .repo-detail { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: none; align-items: center; justify-content: center; z-index: 50; }
92
135
  .repo-detail.open { display: flex; }
93
- .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; }
94
- .repo-detail-box h2 { font-size: 14px; color: #f0f6fc; margin-bottom: 14px; display: flex; justify-content: space-between; align-items: center; }
95
- .repo-close { cursor: pointer; color: #8b949e; font-size: 20px; line-height: 1; border: none; background: none; padding: 0 4px; }
96
- .repo-close:hover { color: #f0f6fc; }
97
- .repo-path { font-size: 11px; color: #484f58; word-break: break-all; margin: -8px 0 12px; }
136
+ .repo-detail-box { background: var(--card-bg); border: 1px solid var(--border); border-radius: 10px; padding: 24px; width: 560px; max-width: 92vw; max-height: 86vh; overflow-y: auto; }
137
+ .repo-detail-box h2 { font-size: 14px; color: var(--fg-strong); margin-bottom: 14px; display: flex; justify-content: space-between; align-items: center; }
138
+ .repo-close { cursor: pointer; color: var(--muted); font-size: 20px; line-height: 1; border: none; background: none; padding: 0 4px; }
139
+ .repo-close:hover { color: var(--fg-strong); }
140
+ .repo-path { font-size: 11px; color: var(--dim); word-break: break-all; margin: -8px 0 12px; }
141
+ /* S34: Game Mode tab โ€” leaderboards, MEGA CACHE banner, Opie unlock tile */
142
+ #panel-game .game-leaderboards { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; }
143
+ #panel-game .lb-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 14px; }
144
+ #panel-game .lb-card h3 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); margin-bottom: 10px; font-weight: 600; }
145
+ #panel-game table { width: 100%; border-collapse: collapse; font-size: 13px; }
146
+ #panel-game td { padding: 4px 8px; border-bottom: 1px solid var(--border-soft); }
147
+ #panel-game td.num { text-align: right; font-family: monospace; color: var(--fg-strong); font-weight: 600; }
148
+ #panel-game .lb-meta { color: var(--muted); font-size: 11px; margin-left: 6px; }
149
+ #panel-game .repos-badge { display: inline-block; background: var(--blue); color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; margin-left: 8px; }
150
+ #mega-cache-banner { display: none; background: var(--mega); color: #1a1006; font-weight: 700; padding: 10px 14px; border-radius: 8px; margin: 12px 0; }
151
+ .achievement-tile { display: none; background: linear-gradient(135deg, #f0883e, #ffd700); color: #1a1006; font-weight: 700; padding: 12px 16px; border-radius: 8px; margin: 12px 0; box-shadow: 0 0 16px #f0883e88; }
152
+ .achievement-tile .ach-detail { display: block; font-weight: 500; font-size: 12px; margin-top: 4px; }
153
+ #mega-cache-toast { display: none; position: fixed; top: 16px; left: 50%; transform: translateX(-50%); background: var(--mega); color: #1a1006; font-weight: 700; padding: 10px 18px; border-radius: 8px; z-index: 1000; box-shadow: 0 4px 20px #0008; }
154
+ #mega-cache-toast.show { display: block; animation: mega-flash 0.6s ease-in-out 2; }
155
+ .level-up { animation: level-up-pulse 1.2s ease-in-out; }
156
+ #game-empty { color: var(--dim); font-style: italic; padding: 12px 0; }
157
+ @keyframes level-up-pulse { 0%{transform:scale(1)} 50%{transform:scale(1.08); filter:brightness(1.3)} 100%{transform:scale(1)} }
158
+ @keyframes mega-flash { 0%{background:transparent} 25%{background:var(--mega-bg, gold)} 100%{background:transparent} }
159
+ /* S35: achievements tile row + unlock toast */
160
+ .ach-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 8px; margin: 8px 0 16px; }
161
+ .ach-tile { padding: 10px 12px; border-radius: 8px; border: 1px solid var(--border); font-size: 12px; background: var(--card-bg); }
162
+ .ach-tile.unlocked { background: linear-gradient(135deg, #f0883e, #ffd700); color: #1a1006; font-weight: 700; box-shadow: 0 0 12px #f0883e66; }
163
+ .ach-tile.locked { opacity: .55; }
164
+ .ach-tile.just-unlocked { animation: ach-unlock-pulse 0.6s ease-out; }
165
+ .ach-tile .ach-detail { display: block; font-weight: 500; font-size: 11px; margin-top: 3px; }
166
+ #ach-toast { display: none; position: fixed; top: 16px; left: 50%; transform: translateX(-50%); background: var(--blue); color: #fff; font-weight: 700; padding: 10px 18px; border-radius: 8px; z-index: 1000; box-shadow: 0 4px 20px #0008; }
167
+ #ach-toast.show { display: block; }
168
+ @keyframes ach-unlock-pulse { 0%{transform:scale(.9);opacity:.4} 60%{transform:scale(1.05)} 100%{transform:scale(1);opacity:1} }
98
169
  </style>
99
170
  </head>
100
171
  <body>
@@ -103,11 +174,18 @@ export function dashboardHtml(tierName) {
103
174
 
104
175
  <h1><span>mega-compact</span><span class="tier" id="hdr-tier">${tierName}</span><span class="version-pill">v${dashboardServerVersion}</span><span class="model-pill" id="hdr-model">โ€”</span></h1>
105
176
 
177
+ <div class="settings-strip">
178
+ <label title="Turn game mode on/off (themes the widget + dashboard)"><input type="checkbox" id="set-game-mode"> Game mode</label>
179
+ <label title="Visual theme (applies instantly)">Theme <select id="set-theme">${THEME_OPTIONS}</select></label>
180
+ <label title="TUI widget display density">TUI <select id="set-tui-mode"><option value="full">Full</option><option value="minimal">Minimal</option></select></label>
181
+ </div>
182
+
106
183
  <nav class="tabs">
107
184
  <button class="tab active" data-tab="current">Current repo</button>
108
185
  <button class="tab" data-tab="all">All repos</button>
109
186
  <button class="tab" data-tab="active">Active Repos</button>
110
187
  <button class="tab" data-tab="summary">Summary</button>
188
+ <button class="tab" data-tab="game">Game Mode</button>
111
189
  </nav>
112
190
 
113
191
  <!-- Current repo (existing single-repo view) -->
@@ -235,7 +313,7 @@ export function dashboardHtml(tierName) {
235
313
  <div class="events-wrap" id="events"><div class="empty">connectingโ€ฆ</div></div>
236
314
  </div>
237
315
 
238
- <h2 style="margin-top:24px;font-size:13px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px">All Repositories</h2>
316
+ <h2 style="margin-top:24px;font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px">All Repositories</h2>
239
317
  <table class="repos">
240
318
  <thead>
241
319
  <tr>
@@ -317,7 +395,7 @@ export function dashboardHtml(tierName) {
317
395
  <div class="summary-card"><div class="num" id="sm-bytes">0 B</div><div class="lbl">Compressed-Original</div></div>
318
396
  </div>
319
397
 
320
- <h2 style="margin-top:24px;font-size:13px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px">Savings by Model</h2>
398
+ <h2 style="margin-top:24px;font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px">Savings by Model</h2>
321
399
  <p class="legend-note" style="margin-bottom:10px">How much context &amp; cost mega-compact has reclaimed, grouped by the model you were running. Compression ratio reflects workload/content, not model quality.</p>
322
400
  <table class="repos">
323
401
  <thead>
@@ -344,6 +422,24 @@ export function dashboardHtml(tierName) {
344
422
  <div class="updated" id="sm-updated"></div>
345
423
  </div>
346
424
 
425
+ <!-- Game Mode (S34) โ€” high-score leaderboards, MEGA CACHE banner, Opie unlock -->
426
+ <div class="tab-panel" id="panel-game">
427
+ <h2 style="font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px">High Scores</h2>
428
+ <div id="mega-cache-banner"></div>
429
+ <div id="mega-cache-toast"></div>
430
+ <div class="achievement-tile" id="opie-tile"></div>
431
+ <h3 style="font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-top:16px">Achievements</h3>
432
+ <div id="ach-toast"></div>
433
+ <div id="ach-tiles" class="ach-tiles">loadingโ€ฆ</div>
434
+ <div class="game-leaderboards">
435
+ <div class="lb-card"><h3>Cache % <span class="repos-badge" id="repos-badge"></span></h3><table><tbody id="lb-cache"><tr><td colspan="2" class="repo-none">loadingโ€ฆ</td></tr></tbody></table></div>
436
+ <div class="lb-card"><h3>Dedupe (collapsed)</h3><table><tbody id="lb-dedupe"><tr><td colspan="2" class="repo-none">loadingโ€ฆ</td></tr></tbody></table></div>
437
+ <div class="lb-card"><h3>Turns <span id="turns-level"></span></h3><table><tbody id="lb-turns"><tr><td colspan="2" class="repo-none">loadingโ€ฆ</td></tr></tbody></table></div>
438
+ <div class="lb-card"><h3>MEGA CACHE trophies</h3><table><tbody id="lb-mega_cache"><tr><td colspan="2" class="repo-none">loadingโ€ฆ</td></tr></tbody></table></div>
439
+ </div>
440
+ <div id="game-empty">No scores yet โ€” run a session with game mode on.</div>
441
+ </div>
442
+
347
443
  <script>
348
444
  (function() {
349
445
  var evBox = document.getElementById('events');
@@ -516,7 +612,11 @@ export function dashboardHtml(tierName) {
516
612
  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)
517
613
  }
518
614
  pollSnapshot();
615
+ renderGameScores();
616
+ renderAchievements();
519
617
  setInterval(pollSnapshot, 2000);
618
+ setInterval(renderGameScores, 2000);
619
+ setInterval(renderAchievements, 2000);
520
620
 
521
621
  // SSE for events
522
622
  function connectSSE() {
@@ -731,9 +831,175 @@ export function dashboardHtml(tierName) {
731
831
  pollServers();
732
832
  setInterval(pollServers, 5000);
733
833
 
834
+ // --- Game-mode settings strip (S32) -------------------------------------
835
+ // Polls GET /api/game-state and applies the row to the settings controls +
836
+ // the document theme. On any control change, PUTs a partial patch back. The
837
+ // dashboard server is a detached child with no MegaRuntime ref, so it reads /
838
+ // writes the game_state SQLite row directly; the in-process MegaRuntime picks
839
+ // up the change via its fs.watch cache-eviction watcher (S32).
840
+ var gmCheckbox = document.getElementById('set-game-mode');
841
+ var gmTheme = document.getElementById('set-theme');
842
+ var gmTui = document.getElementById('set-tui-mode');
843
+
844
+ function applyGameState(gs) {
845
+ if (!gs) return;
846
+ if (gs.theme) document.documentElement.dataset.theme = gs.theme;
847
+ if (gmCheckbox) gmCheckbox.checked = !!gs.game_mode_on;
848
+ if (gmTheme) gmTheme.value = gs.theme || 'transparent';
849
+ if (gmTui) gmTui.value = gs.tui_display_mode || 'full';
850
+ }
851
+ function pollGameState() {
852
+ 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)
853
+ }
854
+ function putGameState(patch) {
855
+ 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)
856
+ }
857
+ if (gmCheckbox) gmCheckbox.addEventListener('change', function() { putGameState({ game_mode_on: gmCheckbox.checked }); });
858
+ if (gmTheme) gmTheme.addEventListener('change', function() { putGameState({ theme: gmTheme.value }); });
859
+ if (gmTui) gmTui.addEventListener('change', function() { putGameState({ tui_display_mode: gmTui.value }); });
860
+ pollGameState();
861
+ setInterval(pollGameState, 5000);
862
+
863
+ // --- Game Mode leaderboards (S34) ----------------------------------------
864
+ // Polls GET /api/game-scores per metric, renders the per-repo leaderboard
865
+ // tables, the MEGA CACHE banner, the hidden Opie's Wild Ride unlock tile, and
866
+ // the transient oopsie toast (fires when a NEW mega_cache trophy row appears
867
+ // since the last poll). Browser-side fetch only (PREVENT-PI-004).
868
+ var GAME_METRICS = ['cache', 'dedupe', 'turns', 'mega_cache'];
869
+ var GAME_EMPTY = true;
870
+ var lastMegaTs = 0;
871
+ var lastMaxLevel = 0;
872
+ function fmtPct(v) { return (Math.round(v * 10) / 10) + '%'; }
873
+ function fmtDate(ts) { return ts ? new Date(ts).toLocaleString() : 'โ€”'; }
874
+ function trophyMeta(m) { try { return (m && typeof m === 'object') ? m : {}; } catch (e) { return {}; } }
875
+ function renderGameScores() {
876
+ var results = {};
877
+ var pending = GAME_METRICS.length + 1; // metrics + repos badge
878
+ function done() {
879
+ if (--pending > 0) return;
880
+ var cache = results['cache'] || [];
881
+ if (cache.length) GAME_EMPTY = false;
882
+ document.getElementById('lb-cache').innerHTML = cache.map(function(r) {
883
+ return '<tr><td title="' + sanitize(r.repo_root) + '">' + sanitize(r.repo_root.split('/').pop()) + '</td><td class="num">' + sanitize(String(r.value)) + '</td></tr>';
884
+ }).join('') || '<tr><td colspan="2" class="repo-none">no data</td></tr>';
885
+ var dedupe = results['dedupe'] || [];
886
+ if (dedupe.length) GAME_EMPTY = false;
887
+ document.getElementById('lb-dedupe').innerHTML = dedupe.map(function(r) {
888
+ return '<tr><td title="' + sanitize(r.repo_root) + '">' + sanitize(r.repo_root.split('/').pop()) + '</td><td class="num">' + sanitize(String(r.value)) + '</td></tr>';
889
+ }).join('') || '<tr><td colspan="2" class="repo-none">no data</td></tr>';
890
+ var turns = results['turns'] || [];
891
+ var maxTurns = turns.reduce(function(mx, r) { return Math.max(mx, r.value); }, 0);
892
+ var lvl = Math.floor(Math.log2(maxTurns + 1)) + 1;
893
+ var lvlEl = document.getElementById('turns-level');
894
+ if (lvlEl) lvlEl.textContent = 'LVL ' + lvl;
895
+ if (turns.length) GAME_EMPTY = false;
896
+ // level-up pulse: when max level increases vs last poll, pulse the cache bar
897
+ if (lvl > lastMaxLevel && lastMaxLevel > 0) {
898
+ var bar = document.getElementById('ctx-bar');
899
+ if (bar) { bar.classList.add('level-up'); setTimeout(function() { bar.classList.remove('level-up'); }, 1200); }
900
+ }
901
+ if (lvl > lastMaxLevel) lastMaxLevel = lvl;
902
+ document.getElementById('lb-turns').innerHTML = turns.map(function(r) {
903
+ return '<tr><td title="' + sanitize(r.repo_root) + '">' + sanitize(r.repo_root.split('/').pop()) + '</td><td class="num">' + sanitize(String(r.value)) + '</td></tr>';
904
+ }).join('') || '<tr><td colspan="2" class="repo-none">no data</td></tr>';
905
+ // mega_cache trophies + banner + Opie tile + transient toast
906
+ var mega = results['mega_cache'] || [];
907
+ var megaBody = document.getElementById('lb-mega_cache');
908
+ if (megaBody) {
909
+ megaBody.innerHTML = mega.map(function(r) {
910
+ var m = trophyMeta(r.meta);
911
+ var fs = m.firstSeenTs || m.firstSeen || r.ts;
912
+ var extra = fs ? ' <span class="lb-meta">' + sanitize(fmtDate(fs)) + '</span>' : '';
913
+ 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>';
914
+ }).join('') || '<tr><td colspan="2" class="repo-none">no trophies yet</td></tr>';
915
+ }
916
+ var banner = document.getElementById('mega-cache-banner');
917
+ var tile = document.getElementById('opie-tile');
918
+ var best = null, firstSeen = null;
919
+ mega.forEach(function(r) {
920
+ if (best == null || r.value > best) best = r.value;
921
+ var m = trophyMeta(r.meta);
922
+ var fs = m.firstSeenTs || m.firstSeen || r.ts;
923
+ if (firstSeen == null || fs < firstSeen) firstSeen = fs;
924
+ });
925
+ if (banner) {
926
+ if (best != null && best > 100) {
927
+ banner.style.display = 'block';
928
+ banner.textContent = '๐Ÿฅง MEGA CACHE! peak ' + fmtPct(best) + ' โ€” first reached ' + fmtDate(firstSeen);
929
+ } else { banner.style.display = 'none'; }
930
+ }
931
+ if (tile) {
932
+ if (best != null && best > 100) {
933
+ tile.style.display = 'block';
934
+ tile.className = 'achievement-tile unlocked';
935
+ tile.innerHTML = '๐Ÿ† Opie\'s Wild Ride<span class="ach-detail">best ' + sanitize(fmtPct(best)) + ' ยท first ' + sanitize(fmtDate(firstSeen)) + '</span>';
936
+ } else { tile.style.display = 'none'; tile.className = 'achievement-tile'; }
937
+ }
938
+ // transient oopsie toast: a NEW mega_cache trophy row since the last poll
939
+ var maxTs = mega.reduce(function(mx, r) { return Math.max(mx, r.ts); }, 0);
940
+ var newRow = mega.find(function(r) { return r.ts > lastMegaTs && r.value > 100; });
941
+ if (lastMegaTs && newRow) {
942
+ var toast = document.getElementById('mega-cache-toast');
943
+ if (toast) {
944
+ toast.textContent = 'oopsie! cache went to ' + Math.round(newRow.value) + '% โ€” MEGA CACHE ๐Ÿฅง';
945
+ toast.classList.add('show');
946
+ setTimeout(function() { toast.classList.remove('show'); }, 4000);
947
+ }
948
+ }
949
+ if (maxTs > lastMegaTs) lastMegaTs = maxTs;
950
+ // empty state
951
+ var emptyEl = document.getElementById('game-empty');
952
+ if (emptyEl) emptyEl.style.display = GAME_EMPTY ? 'block' : 'none';
953
+ }
954
+ GAME_METRICS.forEach(function(m) {
955
+ 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);
956
+ });
957
+ fetch('/api/game-scores?metric=repos&limit=1').then(function(r) { return r.ok ? r.json() : []; }).then(function(rows) {
958
+ var badge = document.getElementById('repos-badge');
959
+ if (badge) badge.textContent = ((rows && rows.length) ? rows[0].value : 0) + ' repos';
960
+ if (rows && rows.length && rows[0].value > 0) GAME_EMPTY = false;
961
+ }).catch(function() {}).then(done);
962
+ }
963
+
964
+ // --- Achievements tile row (S35) ------------------------------------------
965
+ // Polls GET /api/achievements; renders the tile row (hidden+locked render
966
+ // NOTHING; unlocked show icon+title+date; visible-but-locked show ??? teaser)
967
+ // and fires a transient toast when a newly-unlocked achievement appears.
968
+ // Browser-side fetch only (PREVENT-PI-004).
969
+ var lastAchMaxTs = 0;
970
+ function renderAchievements() {
971
+ 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)
972
+ var box = document.getElementById('ach-tiles');
973
+ if (!box) return;
974
+ var html = '';
975
+ var maxTs = 0;
976
+ (rows || []).forEach(function(a) {
977
+ if (a.hidden === 1 && a.unlocked_at == null) return; // hidden invariant: render nothing
978
+ if (a.unlocked_at != null) {
979
+ var isNew = a.unlocked_at > lastAchMaxTs && lastAchMaxTs > 0;
980
+ maxTs = Math.max(maxTs, a.unlocked_at);
981
+ 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>';
982
+ } else {
983
+ html += '<div class="ach-tile locked">??? ' + sanitize(a.title) + '</div>';
984
+ }
985
+ });
986
+ box.innerHTML = html || '<span class="repo-none">no achievements yet</span>';
987
+ var newly = (rows || []).filter(function(a) { return a.unlocked_at != null && a.unlocked_at > lastAchMaxTs; });
988
+ if (lastAchMaxTs && newly.length) {
989
+ var toast = document.getElementById('ach-toast');
990
+ if (toast) {
991
+ toast.textContent = newly.map(function(a) { return (a.icon || '') + ' ' + a.title; }).join(', ') + ' unlocked!';
992
+ toast.classList.add('show');
993
+ setTimeout(function() { toast.classList.remove('show'); }, 4000);
994
+ }
995
+ }
996
+ if (maxTs > lastAchMaxTs) lastAchMaxTs = maxTs;
997
+ }).catch(function() {});
998
+ }
999
+
734
1000
  // --- Tab switching ------------------------------------------------------
735
1001
  var tabs = document.querySelectorAll('.tab');
736
- var panels = { current: 'panel-current', all: 'panel-all', active: 'panel-active', summary: 'panel-summary' };
1002
+ var panels = { current: 'panel-current', all: 'panel-all', active: 'panel-active', summary: 'panel-summary', game: 'panel-game' };
737
1003
  for (var i = 0; i < tabs.length; i++) {
738
1004
  tabs[i].addEventListener('click', function() {
739
1005
  var name = this.getAttribute('data-tab');
@@ -747,6 +1013,7 @@ export function dashboardHtml(tierName) {
747
1013
  }
748
1014
  if (name === 'all' || name === 'summary') pollIndex();
749
1015
  if (name === 'active') pollServers();
1016
+ if (name === 'game') { renderGameScores(); renderAchievements(); }
750
1017
  });
751
1018
  }
752
1019
  })();
@@ -0,0 +1,41 @@
1
+ /**
2
+ * html.test.ts โ€” S34 Game Mode tab presence in the dashboard HTML template.
3
+ */
4
+ import { test, describe } from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { dashboardHtml } from "./html.js";
7
+ describe("S34 dashboard HTML Game Mode", () => {
8
+ const html = dashboardHtml("custom");
9
+ test("Game Mode tab button + panel-game exist", () => {
10
+ assert.ok(html.includes('<button class="tab" data-tab="game">Game Mode</button>'), "game tab button present");
11
+ assert.ok(html.includes('<div class="tab-panel" id="panel-game">'), "panel-game present");
12
+ });
13
+ test("level-up-pulse + mega-flash keyframes present", () => {
14
+ assert.ok(html.includes("@keyframes level-up-pulse"), "level-up-pulse keyframe present");
15
+ assert.ok(html.includes("@keyframes mega-flash"), "mega-flash keyframe present");
16
+ });
17
+ test("empty-state string present", () => {
18
+ assert.ok(html.includes("No scores yet โ€” run a session with game mode on."), "empty-state string present");
19
+ });
20
+ });
21
+ describe("S35 dashboard HTML achievements", () => {
22
+ const html = dashboardHtml("custom");
23
+ test("Achievements heading + ach-tiles/ach-toast containers present", () => {
24
+ assert.ok(html.includes(">Achievements</h3>"), "Achievements heading present");
25
+ assert.ok(html.includes('id="ach-tiles"'), "ach-tiles container present");
26
+ assert.ok(html.includes('id="ach-toast"'), "ach-toast element present");
27
+ });
28
+ test("ach-unlock-pulse keyframe + ach-tile classes present", () => {
29
+ assert.ok(html.includes("@keyframes ach-unlock-pulse"), "ach-unlock-pulse keyframe present");
30
+ assert.ok(html.includes(".ach-tile"), "ach-tile class present");
31
+ assert.ok(html.includes(".ach-tile.unlocked"), "unlocked class present");
32
+ assert.ok(html.includes(".ach-tile.locked"), "locked class present");
33
+ });
34
+ test("renderAchievements fn + GET /api/achievements fetch present", () => {
35
+ assert.ok(html.includes("function renderAchievements"), "renderAchievements fn present");
36
+ assert.ok(html.includes("fetch('/api/achievements')"), "achievements fetch present");
37
+ });
38
+ test("visible-but-locked teaser string present", () => {
39
+ assert.ok(html.includes("??? "), "??? teaser present");
40
+ });
41
+ });