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.
- package/dist/extensions/dashboard-server/html.js +333 -66
- package/dist/extensions/dashboard-server/html.test.js +41 -0
- package/dist/extensions/dashboard-server/server.js +161 -1
- package/dist/extensions/dashboard-server/server.test.js +120 -0
- package/dist/extensions/dashboard-server-s32.test.js +181 -0
- package/dist/extensions/mega-compact.js +3 -0
- package/dist/extensions/mega-events/agent-handlers.js +49 -0
- package/dist/extensions/mega-events/compact-handlers.js +29 -0
- package/dist/extensions/mega-game-cmds.js +106 -0
- package/dist/extensions/mega-game-cmds.test.js +113 -0
- package/dist/extensions/mega-runtime/state.js +167 -1
- package/dist/extensions/mega-runtime/state.test.js +171 -0
- package/dist/extensions/mega-runtime/widget.js +93 -20
- package/dist/extensions/mega-runtime/widget.test.js +160 -0
- package/dist/src/config/themes.js +84 -0
- package/dist/src/config/themes.test.js +94 -0
- package/dist/src/game/scoring.js +105 -0
- package/dist/src/game/scoring.test.js +98 -0
- package/dist/src/store/sqlite/game-achievements.js +111 -0
- package/dist/src/store/sqlite/game-achievements.test.js +67 -0
- package/dist/src/store/sqlite/game-scores.js +105 -0
- package/dist/src/store/sqlite/game-scores.test.js +106 -0
- package/dist/src/store/sqlite/game-state.js +54 -0
- package/dist/src/store/sqlite/game-state.test.js +76 -0
- package/dist/src/store/sqlite/schema.js +44 -0
- package/dist/src/store/sqlite.js +3 -0
- package/extensions/dashboard-server/html.test.ts +50 -0
- package/extensions/dashboard-server/html.ts +334 -66
- package/extensions/dashboard-server/server.test.ts +131 -0
- package/extensions/dashboard-server/server.ts +148 -1
- package/extensions/dashboard-server-s32.test.ts +195 -0
- package/extensions/mega-compact.ts +3 -0
- package/extensions/mega-events/agent-handlers.ts +48 -0
- package/extensions/mega-events/compact-handlers.ts +28 -0
- package/extensions/mega-game-cmds.test.ts +137 -0
- package/extensions/mega-game-cmds.ts +122 -0
- package/extensions/mega-runtime/state.test.ts +171 -0
- package/extensions/mega-runtime/state.ts +170 -0
- package/extensions/mega-runtime/widget.test.ts +185 -0
- package/extensions/mega-runtime/widget.ts +119 -18
- package/package.json +1 -1
- package/src/config/themes.test.ts +116 -0
- package/src/config/themes.ts +124 -0
- package/src/game/scoring.test.ts +103 -0
- package/src/game/scoring.ts +158 -0
- package/src/store/sqlite/game-achievements.test.ts +80 -0
- package/src/store/sqlite/game-achievements.ts +147 -0
- package/src/store/sqlite/game-scores.test.ts +132 -0
- package/src/store/sqlite/game-scores.ts +168 -0
- package/src/store/sqlite/game-state.test.ts +89 -0
- package/src/store/sqlite/game-state.ts +87 -0
- package/src/store/sqlite/schema.ts +47 -0
- package/src/store/sqlite.ts +3 -0
- package/dist/extensions/dashboard-server/helpers.js +0 -37
- package/dist/extensions/dashboard-server/html/all-repos-tab.js +0 -26
- package/dist/extensions/dashboard-server/html/body-open.js +0 -23
- package/dist/extensions/dashboard-server/html/current-repo-tab.js +0 -130
- package/dist/extensions/dashboard-server/html/head-open.js +0 -16
- package/dist/extensions/dashboard-server/html/high-score-tab.js +0 -25
- package/dist/extensions/dashboard-server/html/repo-detail-modal.js +0 -26
- package/dist/extensions/dashboard-server/html/script.js +0 -259
- package/dist/extensions/dashboard-server/html/styles.js +0 -103
- package/dist/extensions/dashboard-server/html/summary-tab.js +0 -19
- package/dist/extensions/dashboard-server/html-template.js +0 -41
- package/dist/src/store/sqlite/connection.js +0 -35
- package/dist/src/store/sqlite/index-store.js +0 -167
- package/dist/src/store/sqlite/memory.js +0 -54
- package/dist/src/store/sqlite/minhash-lsh.js +0 -47
- package/dist/src/store/sqlite/sessions.js +0 -39
- package/dist/src/store/sqlite/transaction.js +0 -19
- package/dist/src/vectorStore/add.js +0 -260
- package/dist/src/vectorStore/dedup.js +0 -52
- package/dist/src/vectorStore/index.js +0 -10
- package/dist/src/vectorStore/queries.js +0 -83
- package/dist/src/vectorStore/search.js +0 -95
- package/dist/src/vectorStore/session.js +0 -19
- package/dist/src/vectorStore/store.js +0 -105
- package/dist/src/vectorStore/types.js +0 -6
- 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:
|
|
15
|
-
h1 { font-size: 20px; font-weight: 600; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; color:
|
|
16
|
-
h1 .tier { background:
|
|
17
|
-
h1 .version-pill { background:
|
|
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:
|
|
20
|
-
.card.safe { border-color:
|
|
21
|
-
.card.safe h2 { color:
|
|
22
|
-
.safe-note { font-size: 12px; color:
|
|
23
|
-
.value.ok { color:
|
|
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
|
|
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:
|
|
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:
|
|
32
|
-
.legend-note { font-size: 12px; color:
|
|
33
|
-
.card h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color:
|
|
34
|
-
.meter-track { background:
|
|
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:
|
|
37
|
-
.meter-yellow { background:
|
|
38
|
-
.meter-red { background:
|
|
39
|
-
.meter-label { font-size: 24px; font-weight: 700; color:
|
|
40
|
-
.meter-sub { font-size: 12px; color:
|
|
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:
|
|
44
|
-
.bullet-off { background:
|
|
45
|
-
.bullet-na { background:
|
|
46
|
-
.state-text { font-size: 13px; color:
|
|
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:
|
|
49
|
-
.stat-grid .value { color:
|
|
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:
|
|
52
|
-
.conf-grid .value { color:
|
|
53
|
-
.events { background:
|
|
54
|
-
.events h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color:
|
|
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
|
|
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:
|
|
60
|
-
.ev-type-recall { color:
|
|
61
|
-
.ev-time { color:
|
|
62
|
-
.ev-detail { color:
|
|
63
|
-
.updated { font-size: 11px; color:
|
|
64
|
-
.empty { color:
|
|
65
|
-
.offline-banner { background: #f8514922; border: 1px solid
|
|
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:
|
|
68
|
-
.tab:hover { color:
|
|
69
|
-
.tab.active { background:
|
|
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:
|
|
74
|
-
.summary-card .num { font-size: 24px; font-weight: 700; color:
|
|
75
|
-
.summary-card .lbl { font-size: 12px; color:
|
|
76
|
-
table.repos { width: 100%; border-collapse: collapse; background:
|
|
77
|
-
table.repos th, table.repos td { text-align: left; padding: 10px 14px; font-size: 13px; border-bottom: 1px solid
|
|
78
|
-
table.repos th { color:
|
|
79
|
-
table.repos td.num { font-family: monospace; color:
|
|
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:
|
|
82
|
-
.repo-model { color:
|
|
83
|
-
.repo-none { color:
|
|
84
|
-
.updated { font-size: 11px; color:
|
|
85
|
-
.model-pill { background:
|
|
86
|
-
.card.cost h2 { color:
|
|
87
|
-
.cost-usd { font-size: 22px; font-weight: 700; color:
|
|
88
|
-
.cost-sub { font-size: 12px; color:
|
|
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:
|
|
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:
|
|
94
|
-
.repo-detail-box h2 { font-size: 14px; color:
|
|
95
|
-
.repo-close { cursor: pointer; color:
|
|
96
|
-
.repo-close:hover { color:
|
|
97
|
-
.repo-path { font-size: 11px; color:
|
|
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
|
|
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
|
|
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 & 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
|
+
});
|