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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ACHIEVEMENT_DEFS } from "../../game/scoring.js";
|
|
1
2
|
const SCHEMA_VERSION = 2;
|
|
2
3
|
/**
|
|
3
4
|
* Add `column` (with `decl`, e.g. "INTEGER") to `table` if it does not already
|
|
@@ -225,6 +226,41 @@ export function initSchema(db) {
|
|
|
225
226
|
first_seen_seq INTEGER NOT NULL,
|
|
226
227
|
created_at INTEGER NOT NULL
|
|
227
228
|
);
|
|
229
|
+
|
|
230
|
+
-- S30 game mode: global toggle state (single row, id=1). Holds the
|
|
231
|
+
-- game-mode on/off switch, the active visual-effect theme id, and the TUI
|
|
232
|
+
-- widget display mode. Global across all repos; written by /mega-game and
|
|
233
|
+
-- the dashboard settings strip (S32). Local SQLite (PREVENT-PI-004).
|
|
234
|
+
CREATE TABLE IF NOT EXISTS game_state (
|
|
235
|
+
id INTEGER PRIMARY KEY CHECK(id = 1),
|
|
236
|
+
game_mode_on INTEGER NOT NULL DEFAULT 0,
|
|
237
|
+
theme TEXT NOT NULL DEFAULT 'transparent',
|
|
238
|
+
tui_display_mode TEXT NOT NULL DEFAULT 'full'
|
|
239
|
+
CHECK(tui_display_mode IN ('full','minimal'))
|
|
240
|
+
);
|
|
241
|
+
-- S33 game mode: per-repo leaderboard metrics. One row per recorded event
|
|
242
|
+
-- (turn_end / session_compact); leaderboard() derives rankings. 'repos' is
|
|
243
|
+
-- derived (COUNT DISTINCT, never recorded). Local SQLite (PREVENT-PI-004).
|
|
244
|
+
CREATE TABLE IF NOT EXISTS game_scores (
|
|
245
|
+
repo_root TEXT NOT NULL,
|
|
246
|
+
metric TEXT NOT NULL CHECK(metric IN ('cache','dedupe','turns','repos','mega_cache')),
|
|
247
|
+
ts INTEGER NOT NULL,
|
|
248
|
+
value REAL NOT NULL,
|
|
249
|
+
meta TEXT,
|
|
250
|
+
PRIMARY KEY(repo_root, metric, ts)
|
|
251
|
+
) WITHOUT ROWID;
|
|
252
|
+
CREATE INDEX IF NOT EXISTS idx_game_scores_metric_ts ON game_scores(metric, ts);
|
|
253
|
+
|
|
254
|
+
-- S35 game mode: achievements (9 rows, seeded idempotently on first open).
|
|
255
|
+
-- hidden=1 AND unlocked_at IS NULL => render NOTHING (no teaser). Local SQLite.
|
|
256
|
+
CREATE TABLE IF NOT EXISTS game_achievements (
|
|
257
|
+
id TEXT PRIMARY KEY,
|
|
258
|
+
title TEXT NOT NULL,
|
|
259
|
+
description TEXT NOT NULL,
|
|
260
|
+
hidden INTEGER NOT NULL DEFAULT 0 CHECK(hidden IN (0,1)),
|
|
261
|
+
icon TEXT,
|
|
262
|
+
unlocked_at INTEGER NULL
|
|
263
|
+
) WITHOUT ROWID;
|
|
228
264
|
`);
|
|
229
265
|
// Idempotent column migrations. `CREATE TABLE IF NOT EXISTS` is a no-op on a
|
|
230
266
|
// pre-existing table, so new columns added to context_chunks after a store was
|
|
@@ -243,6 +279,14 @@ export function initSchema(db) {
|
|
|
243
279
|
// S25: RAPTOR freshness-guard timestamp. Additive; old DBs have NULL โ 0 โ
|
|
244
280
|
// treated as stale โ flat fallback (safe).
|
|
245
281
|
ensureColumn(db, "raptor_nodes", "built_at", "INTEGER");
|
|
282
|
+
// S35: idempotent seed of the 9 achievement rows. ON CONFLICT(id) DO
|
|
283
|
+
// NOTHING so a re-open never clobbers an already-unlocked row's
|
|
284
|
+
// unlocked_at. No user input reaches this SQL (PREVENT-002 safe).
|
|
285
|
+
const seedAch = db.prepare(`INSERT INTO game_achievements (id, title, description, hidden, icon)
|
|
286
|
+
VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING`);
|
|
287
|
+
for (const d of ACHIEVEMENT_DEFS) {
|
|
288
|
+
seedAch.run(d.id, d.title, d.description, d.hidden ? 1 : 0, d.icon);
|
|
289
|
+
}
|
|
246
290
|
const v = db.prepare("SELECT value FROM meta WHERE key='schema_version'").get();
|
|
247
291
|
if (!v) {
|
|
248
292
|
db.prepare("INSERT INTO meta(key, value) VALUES(?, ?)").run("schema_version", String(SCHEMA_VERSION));
|
package/dist/src/store/sqlite.js
CHANGED
|
@@ -20,3 +20,6 @@ export * from "./sqlite/raptor.js";
|
|
|
20
20
|
export * from "./sqlite/raw-transcript.js";
|
|
21
21
|
export * from "./sqlite/dedup-mirror.js";
|
|
22
22
|
export * from "./sqlite/maintenance.js";
|
|
23
|
+
export * from "./sqlite/game-state.js";
|
|
24
|
+
export * from "./sqlite/game-scores.js";
|
|
25
|
+
export * from "./sqlite/game-achievements.js";
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
|
|
8
|
+
describe("S34 dashboard HTML Game Mode", () => {
|
|
9
|
+
const html = dashboardHtml("custom");
|
|
10
|
+
test("Game Mode tab button + panel-game exist", () => {
|
|
11
|
+
assert.ok(
|
|
12
|
+
html.includes('<button class="tab" data-tab="game">Game Mode</button>'),
|
|
13
|
+
"game tab button present",
|
|
14
|
+
);
|
|
15
|
+
assert.ok(html.includes('<div class="tab-panel" id="panel-game">'), "panel-game present");
|
|
16
|
+
});
|
|
17
|
+
test("level-up-pulse + mega-flash keyframes present", () => {
|
|
18
|
+
assert.ok(html.includes("@keyframes level-up-pulse"), "level-up-pulse keyframe present");
|
|
19
|
+
assert.ok(html.includes("@keyframes mega-flash"), "mega-flash keyframe present");
|
|
20
|
+
});
|
|
21
|
+
test("empty-state string present", () => {
|
|
22
|
+
assert.ok(
|
|
23
|
+
html.includes("No scores yet โ run a session with game mode on."),
|
|
24
|
+
"empty-state string present",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
describe("S35 dashboard HTML achievements", () => {
|
|
31
|
+
const html = dashboardHtml("custom");
|
|
32
|
+
test("Achievements heading + ach-tiles/ach-toast containers present", () => {
|
|
33
|
+
assert.ok(html.includes(">Achievements</h3>"), "Achievements heading present");
|
|
34
|
+
assert.ok(html.includes('id="ach-tiles"'), "ach-tiles container present");
|
|
35
|
+
assert.ok(html.includes('id="ach-toast"'), "ach-toast element present");
|
|
36
|
+
});
|
|
37
|
+
test("ach-unlock-pulse keyframe + ach-tile classes present", () => {
|
|
38
|
+
assert.ok(html.includes("@keyframes ach-unlock-pulse"), "ach-unlock-pulse keyframe present");
|
|
39
|
+
assert.ok(html.includes(".ach-tile"), "ach-tile class present");
|
|
40
|
+
assert.ok(html.includes(".ach-tile.unlocked"), "unlocked class present");
|
|
41
|
+
assert.ok(html.includes(".ach-tile.locked"), "locked class present");
|
|
42
|
+
});
|
|
43
|
+
test("renderAchievements fn + GET /api/achievements fetch present", () => {
|
|
44
|
+
assert.ok(html.includes("function renderAchievements"), "renderAchievements fn present");
|
|
45
|
+
assert.ok(html.includes("fetch('/api/achievements')"), "achievements fetch present");
|
|
46
|
+
});
|
|
47
|
+
test("visible-but-locked teaser string present", () => {
|
|
48
|
+
assert.ok(html.includes("??? "), "??? teaser present");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -3,100 +3,172 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { dashboardServerVersion } from "./state.js";
|
|
6
|
+
import { THEMES, DEFAULT_THEME, themeDataBlock } from "../../src/config/themes.js";
|
|
7
|
+
|
|
8
|
+
// Server-side injection of every theme's :root[data-theme="<id>"] CSS-var
|
|
9
|
+
// override block so the client can switch themes instantly by setting
|
|
10
|
+
// document.documentElement.dataset.theme. PREVENT-PI-004: pure local, no network.
|
|
11
|
+
const THEME_STYLE_BLOCKS = THEMES.map(themeDataBlock).join("\n");
|
|
12
|
+
const THEME_OPTIONS = THEMES.map((t) => `<option value="${t.id}">${t.label}</option>`).join("");
|
|
6
13
|
|
|
7
14
|
export function dashboardHtml(tierName: string): string {
|
|
8
15
|
return `<!DOCTYPE html>
|
|
9
|
-
<html lang="en">
|
|
16
|
+
<html lang="en" data-theme="${DEFAULT_THEME}">
|
|
10
17
|
<head>
|
|
11
18
|
<meta charset="utf-8">
|
|
12
19
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
13
20
|
<title>mega-compact dashboard</title>
|
|
14
21
|
<style>
|
|
22
|
+
/* S32: CSS-variable skin. Base :root holds the CURRENT hardcoded hexes so the
|
|
23
|
+
default (data-theme="transparent") is visually identical to the pre-S32
|
|
24
|
+
dashboard. Each :root[data-theme="<id>"] block (injected below from
|
|
25
|
+
src/config/themes.ts) overrides the 4 theme vars (--bg/--fg/--accent/--mega).
|
|
26
|
+
Non-theme palette tokens (card bg, borders, muted text, meter colors) stay
|
|
27
|
+
fixed so visual parity is preserved under the transparent default. */
|
|
28
|
+
:root {
|
|
29
|
+
--bg: #0d1117; /* page background (transparent theme -> transparent) */
|
|
30
|
+
--fg: #c9d1d9; /* default foreground */
|
|
31
|
+
--accent: #3fb950; /* accent (bars, ok values, on-bullets) */
|
|
32
|
+
--mega: #f0883e; /* MEGA CACHE highlight */
|
|
33
|
+
--fg-strong: #f0f6fc; /* headings, strong values */
|
|
34
|
+
--muted: #8b949e; /* labels, sub-text */
|
|
35
|
+
--dim: #484f58; /* timestamps, empty states */
|
|
36
|
+
--card-bg: #161b22; /* card / events / table background */
|
|
37
|
+
--border: #30363d; /* card / table borders */
|
|
38
|
+
--border-soft: #21262d; /* meter track, ev borders, table row borders */
|
|
39
|
+
--blue: #1f6feb; /* tier pill, active tab */
|
|
40
|
+
--green-bar: #238636; /* green meter fill, safe border */
|
|
41
|
+
--yellow-bar: #d29922; /* yellow meter fill, na-bullet */
|
|
42
|
+
--red-bar: #f85149; /* red meter fill, offline banner */
|
|
43
|
+
--purple: #a371f7; /* recall events, repo model, cost h2 */
|
|
44
|
+
--purple-pill: #6e40c9; /* model pill bg */
|
|
45
|
+
--hover-row: #1c2128; /* table row hover */
|
|
46
|
+
--link: #58a6ff; /* repo-link hover */
|
|
47
|
+
--th-bg: #0d1117; /* table header background */
|
|
48
|
+
}
|
|
49
|
+
/* html backdrop keeps the page dark even when --bg is transparent (transparent
|
|
50
|
+
theme) so there's no white flash โ visually identical to the pre-S32 fill. */
|
|
51
|
+
html { background: #0d1117; }
|
|
52
|
+
${THEME_STYLE_BLOCKS}
|
|
15
53
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
16
|
-
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background:
|
|
17
|
-
h1 { font-size: 20px; font-weight: 600; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; color:
|
|
18
|
-
h1 .tier { background:
|
|
19
|
-
h1 .version-pill { background:
|
|
54
|
+
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; }
|
|
55
|
+
h1 { font-size: 20px; font-weight: 600; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; color: var(--fg-strong); }
|
|
56
|
+
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; }
|
|
57
|
+
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; }
|
|
20
58
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px; }
|
|
21
|
-
.card { background:
|
|
22
|
-
.card.safe { border-color:
|
|
23
|
-
.card.safe h2 { color:
|
|
24
|
-
.safe-note { font-size: 12px; color:
|
|
25
|
-
.value.ok { color:
|
|
59
|
+
.card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
|
|
60
|
+
.card.safe { border-color: var(--green-bar); }
|
|
61
|
+
.card.safe h2 { color: var(--accent); }
|
|
62
|
+
.safe-note { font-size: 12px; color: var(--muted); margin: 12px 0 0; line-height: 1.5; }
|
|
63
|
+
.value.ok { color: var(--accent); }
|
|
26
64
|
.label {
|
|
27
65
|
cursor: help;
|
|
28
|
-
border-bottom: 1px dotted
|
|
66
|
+
border-bottom: 1px dotted var(--dim);
|
|
29
67
|
}
|
|
30
68
|
.card.legend { grid-column: 1 / -1; }
|
|
31
|
-
.legend-list { margin: 0; padding-left: 18px; color:
|
|
69
|
+
.legend-list { margin: 0; padding-left: 18px; color: var(--fg); }
|
|
32
70
|
.legend-list li { margin-bottom: 8px; line-height: 1.5; }
|
|
33
|
-
.legend-list b { color:
|
|
34
|
-
.legend-note { font-size: 12px; color:
|
|
35
|
-
.card h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color:
|
|
36
|
-
.meter-track { background:
|
|
71
|
+
.legend-list b { color: var(--fg-strong); }
|
|
72
|
+
.legend-note { font-size: 12px; color: var(--muted); margin: 12px 0 0; font-style: italic; }
|
|
73
|
+
.card h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); margin-bottom: 12px; font-weight: 600; }
|
|
74
|
+
.meter-track { background: var(--border-soft); border-radius: 4px; height: 20px; overflow: hidden; margin: 8px 0; }
|
|
37
75
|
.meter-fill { height: 100%; border-radius: 4px; transition: width .6s ease; min-width: 2px; }
|
|
38
|
-
.meter-green { background:
|
|
39
|
-
.meter-yellow { background:
|
|
40
|
-
.meter-red { background:
|
|
41
|
-
.meter-label { font-size: 24px; font-weight: 700; color:
|
|
42
|
-
.meter-sub { font-size: 12px; color:
|
|
76
|
+
.meter-green { background: var(--green-bar); }
|
|
77
|
+
.meter-yellow { background: var(--yellow-bar); }
|
|
78
|
+
.meter-red { background: var(--red-bar); }
|
|
79
|
+
.meter-label { font-size: 24px; font-weight: 700; color: var(--fg-strong); }
|
|
80
|
+
.meter-sub { font-size: 12px; color: var(--muted); }
|
|
43
81
|
.status-row { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-size: 14px; }
|
|
44
82
|
.status-row .bullet { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
45
|
-
.bullet-on { background:
|
|
46
|
-
.bullet-off { background:
|
|
47
|
-
.bullet-na { background:
|
|
48
|
-
.state-text { font-size: 13px; color:
|
|
83
|
+
.bullet-on { background: var(--accent); box-shadow: 0 0 6px #3fb95088; }
|
|
84
|
+
.bullet-off { background: var(--dim); }
|
|
85
|
+
.bullet-na { background: var(--yellow-bar); }
|
|
86
|
+
.state-text { font-size: 13px; color: var(--muted); margin-top: 8px; font-family: monospace; }
|
|
49
87
|
.stat-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 14px; }
|
|
50
|
-
.stat-grid .label { color:
|
|
51
|
-
.stat-grid .value { color:
|
|
88
|
+
.stat-grid .label { color: var(--muted); }
|
|
89
|
+
.stat-grid .value { color: var(--fg-strong); font-weight: 600; font-family: monospace; }
|
|
52
90
|
.conf-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 14px; }
|
|
53
|
-
.conf-grid .label { color:
|
|
54
|
-
.conf-grid .value { color:
|
|
55
|
-
.events { background:
|
|
56
|
-
.events h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color:
|
|
91
|
+
.conf-grid .label { color: var(--muted); }
|
|
92
|
+
.conf-grid .value { color: var(--fg-strong); font-family: monospace; }
|
|
93
|
+
.events { background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
|
|
94
|
+
.events h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); margin-bottom: 12px; font-weight: 600; }
|
|
57
95
|
.events-wrap { max-height: 240px; overflow-y: auto; font-family: monospace; font-size: 12px; }
|
|
58
|
-
.ev { padding: 3px 0; border-bottom: 1px solid
|
|
96
|
+
.ev { padding: 3px 0; border-bottom: 1px solid var(--border-soft); display: flex; gap: 8px; align-items: baseline; }
|
|
59
97
|
.ev:last-child { border-bottom: none; }
|
|
60
98
|
.ev-type { font-weight: 700; min-width: 70px; text-align: right; }
|
|
61
|
-
.ev-type-compact { color:
|
|
62
|
-
.ev-type-recall { color:
|
|
63
|
-
.ev-time { color:
|
|
64
|
-
.ev-detail { color:
|
|
65
|
-
.updated { font-size: 11px; color:
|
|
66
|
-
.empty { color:
|
|
67
|
-
.offline-banner { background: #f8514922; border: 1px solid
|
|
99
|
+
.ev-type-compact { color: var(--accent); }
|
|
100
|
+
.ev-type-recall { color: var(--purple); }
|
|
101
|
+
.ev-time { color: var(--dim); font-size: 10px; min-width: 80px; }
|
|
102
|
+
.ev-detail { color: var(--muted); flex: 1; }
|
|
103
|
+
.updated { font-size: 11px; color: var(--dim); margin-top: 16px; text-align: right; }
|
|
104
|
+
.empty { color: var(--dim); font-style: italic; font-size: 13px; padding: 8px 0; }
|
|
105
|
+
.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; }
|
|
68
106
|
.tabs { display: flex; gap: 8px; margin-bottom: 20px; }
|
|
69
|
-
.tab { background:
|
|
70
|
-
.tab:hover { color:
|
|
71
|
-
.tab.active { background:
|
|
107
|
+
.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; }
|
|
108
|
+
.tab:hover { color: var(--fg); border-color: var(--dim); }
|
|
109
|
+
.tab.active { background: var(--blue); color: #fff; border-color: var(--blue); }
|
|
72
110
|
.tab-panel { display: none; }
|
|
73
111
|
.tab-panel.active { display: block; }
|
|
112
|
+
/* S32: header settings strip (game-mode toggle + theme + TUI display-mode) */
|
|
113
|
+
.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; }
|
|
114
|
+
.settings-strip label { display: flex; align-items: center; gap: 6px; font-weight: 600; }
|
|
115
|
+
.settings-strip input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; accent-color: var(--accent); }
|
|
116
|
+
.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; }
|
|
117
|
+
.settings-strip select:hover { border-color: var(--dim); }
|
|
74
118
|
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 20px; }
|
|
75
|
-
.summary-card { background:
|
|
76
|
-
.summary-card .num { font-size: 24px; font-weight: 700; color:
|
|
77
|
-
.summary-card .lbl { font-size: 12px; color:
|
|
78
|
-
table.repos { width: 100%; border-collapse: collapse; background:
|
|
79
|
-
table.repos th, table.repos td { text-align: left; padding: 10px 14px; font-size: 13px; border-bottom: 1px solid
|
|
80
|
-
table.repos th { color:
|
|
81
|
-
table.repos td.num { font-family: monospace; color:
|
|
119
|
+
.summary-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
|
|
120
|
+
.summary-card .num { font-size: 24px; font-weight: 700; color: var(--fg-strong); }
|
|
121
|
+
.summary-card .lbl { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; margin-top: 4px; }
|
|
122
|
+
table.repos { width: 100%; border-collapse: collapse; background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
123
|
+
table.repos th, table.repos td { text-align: left; padding: 10px 14px; font-size: 13px; border-bottom: 1px solid var(--border-soft); }
|
|
124
|
+
table.repos th { color: var(--muted); text-transform: uppercase; letter-spacing: .5px; font-size: 11px; background: var(--th-bg); }
|
|
125
|
+
table.repos td.num { font-family: monospace; color: var(--fg-strong); text-align: right; }
|
|
82
126
|
table.repos tr:last-child td { border-bottom: none; }
|
|
83
|
-
table.repos tr:hover td { background:
|
|
84
|
-
.repo-model { color:
|
|
85
|
-
.repo-none { color:
|
|
86
|
-
.updated { font-size: 11px; color:
|
|
87
|
-
.model-pill { background:
|
|
88
|
-
.card.cost h2 { color:
|
|
89
|
-
.cost-usd { font-size: 22px; font-weight: 700; color:
|
|
90
|
-
.cost-sub { font-size: 12px; color:
|
|
127
|
+
table.repos tr:hover td { background: var(--hover-row); }
|
|
128
|
+
.repo-model { color: var(--purple); }
|
|
129
|
+
.repo-none { color: var(--dim); font-style: italic; }
|
|
130
|
+
.updated { font-size: 11px; color: var(--dim); margin-top: 16px; text-align: right; }
|
|
131
|
+
.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; }
|
|
132
|
+
.card.cost h2 { color: var(--purple); }
|
|
133
|
+
.cost-usd { font-size: 22px; font-weight: 700; color: var(--accent); }
|
|
134
|
+
.cost-sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
|
|
91
135
|
.repo-link { cursor: pointer; }
|
|
92
|
-
.repo-link:hover td { color:
|
|
136
|
+
.repo-link:hover td { color: var(--link); }
|
|
93
137
|
.repo-detail { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: none; align-items: center; justify-content: center; z-index: 50; }
|
|
94
138
|
.repo-detail.open { display: flex; }
|
|
95
|
-
.repo-detail-box { background:
|
|
96
|
-
.repo-detail-box h2 { font-size: 14px; color:
|
|
97
|
-
.repo-close { cursor: pointer; color:
|
|
98
|
-
.repo-close:hover { color:
|
|
99
|
-
.repo-path { font-size: 11px; color:
|
|
139
|
+
.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; }
|
|
140
|
+
.repo-detail-box h2 { font-size: 14px; color: var(--fg-strong); margin-bottom: 14px; display: flex; justify-content: space-between; align-items: center; }
|
|
141
|
+
.repo-close { cursor: pointer; color: var(--muted); font-size: 20px; line-height: 1; border: none; background: none; padding: 0 4px; }
|
|
142
|
+
.repo-close:hover { color: var(--fg-strong); }
|
|
143
|
+
.repo-path { font-size: 11px; color: var(--dim); word-break: break-all; margin: -8px 0 12px; }
|
|
144
|
+
/* S34: Game Mode tab โ leaderboards, MEGA CACHE banner, Opie unlock tile */
|
|
145
|
+
#panel-game .game-leaderboards { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; }
|
|
146
|
+
#panel-game .lb-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 14px; }
|
|
147
|
+
#panel-game .lb-card h3 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); margin-bottom: 10px; font-weight: 600; }
|
|
148
|
+
#panel-game table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
149
|
+
#panel-game td { padding: 4px 8px; border-bottom: 1px solid var(--border-soft); }
|
|
150
|
+
#panel-game td.num { text-align: right; font-family: monospace; color: var(--fg-strong); font-weight: 600; }
|
|
151
|
+
#panel-game .lb-meta { color: var(--muted); font-size: 11px; margin-left: 6px; }
|
|
152
|
+
#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; }
|
|
153
|
+
#mega-cache-banner { display: none; background: var(--mega); color: #1a1006; font-weight: 700; padding: 10px 14px; border-radius: 8px; margin: 12px 0; }
|
|
154
|
+
.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; }
|
|
155
|
+
.achievement-tile .ach-detail { display: block; font-weight: 500; font-size: 12px; margin-top: 4px; }
|
|
156
|
+
#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; }
|
|
157
|
+
#mega-cache-toast.show { display: block; animation: mega-flash 0.6s ease-in-out 2; }
|
|
158
|
+
.level-up { animation: level-up-pulse 1.2s ease-in-out; }
|
|
159
|
+
#game-empty { color: var(--dim); font-style: italic; padding: 12px 0; }
|
|
160
|
+
@keyframes level-up-pulse { 0%{transform:scale(1)} 50%{transform:scale(1.08); filter:brightness(1.3)} 100%{transform:scale(1)} }
|
|
161
|
+
@keyframes mega-flash { 0%{background:transparent} 25%{background:var(--mega-bg, gold)} 100%{background:transparent} }
|
|
162
|
+
/* S35: achievements tile row + unlock toast */
|
|
163
|
+
.ach-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 8px; margin: 8px 0 16px; }
|
|
164
|
+
.ach-tile { padding: 10px 12px; border-radius: 8px; border: 1px solid var(--border); font-size: 12px; background: var(--card-bg); }
|
|
165
|
+
.ach-tile.unlocked { background: linear-gradient(135deg, #f0883e, #ffd700); color: #1a1006; font-weight: 700; box-shadow: 0 0 12px #f0883e66; }
|
|
166
|
+
.ach-tile.locked { opacity: .55; }
|
|
167
|
+
.ach-tile.just-unlocked { animation: ach-unlock-pulse 0.6s ease-out; }
|
|
168
|
+
.ach-tile .ach-detail { display: block; font-weight: 500; font-size: 11px; margin-top: 3px; }
|
|
169
|
+
#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; }
|
|
170
|
+
#ach-toast.show { display: block; }
|
|
171
|
+
@keyframes ach-unlock-pulse { 0%{transform:scale(.9);opacity:.4} 60%{transform:scale(1.05)} 100%{transform:scale(1);opacity:1} }
|
|
100
172
|
</style>
|
|
101
173
|
</head>
|
|
102
174
|
<body>
|
|
@@ -105,11 +177,18 @@ export function dashboardHtml(tierName: string): string {
|
|
|
105
177
|
|
|
106
178
|
<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>
|
|
107
179
|
|
|
180
|
+
<div class="settings-strip">
|
|
181
|
+
<label title="Turn game mode on/off (themes the widget + dashboard)"><input type="checkbox" id="set-game-mode"> Game mode</label>
|
|
182
|
+
<label title="Visual theme (applies instantly)">Theme <select id="set-theme">${THEME_OPTIONS}</select></label>
|
|
183
|
+
<label title="TUI widget display density">TUI <select id="set-tui-mode"><option value="full">Full</option><option value="minimal">Minimal</option></select></label>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
108
186
|
<nav class="tabs">
|
|
109
187
|
<button class="tab active" data-tab="current">Current repo</button>
|
|
110
188
|
<button class="tab" data-tab="all">All repos</button>
|
|
111
189
|
<button class="tab" data-tab="active">Active Repos</button>
|
|
112
190
|
<button class="tab" data-tab="summary">Summary</button>
|
|
191
|
+
<button class="tab" data-tab="game">Game Mode</button>
|
|
113
192
|
</nav>
|
|
114
193
|
|
|
115
194
|
<!-- Current repo (existing single-repo view) -->
|
|
@@ -237,7 +316,7 @@ export function dashboardHtml(tierName: string): string {
|
|
|
237
316
|
<div class="events-wrap" id="events"><div class="empty">connectingโฆ</div></div>
|
|
238
317
|
</div>
|
|
239
318
|
|
|
240
|
-
<h2 style="margin-top:24px;font-size:13px;color
|
|
319
|
+
<h2 style="margin-top:24px;font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px">All Repositories</h2>
|
|
241
320
|
<table class="repos">
|
|
242
321
|
<thead>
|
|
243
322
|
<tr>
|
|
@@ -319,7 +398,7 @@ export function dashboardHtml(tierName: string): string {
|
|
|
319
398
|
<div class="summary-card"><div class="num" id="sm-bytes">0 B</div><div class="lbl">Compressed-Original</div></div>
|
|
320
399
|
</div>
|
|
321
400
|
|
|
322
|
-
<h2 style="margin-top:24px;font-size:13px;color
|
|
401
|
+
<h2 style="margin-top:24px;font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px">Savings by Model</h2>
|
|
323
402
|
<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>
|
|
324
403
|
<table class="repos">
|
|
325
404
|
<thead>
|
|
@@ -346,6 +425,24 @@ export function dashboardHtml(tierName: string): string {
|
|
|
346
425
|
<div class="updated" id="sm-updated"></div>
|
|
347
426
|
</div>
|
|
348
427
|
|
|
428
|
+
<!-- Game Mode (S34) โ high-score leaderboards, MEGA CACHE banner, Opie unlock -->
|
|
429
|
+
<div class="tab-panel" id="panel-game">
|
|
430
|
+
<h2 style="font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px">High Scores</h2>
|
|
431
|
+
<div id="mega-cache-banner"></div>
|
|
432
|
+
<div id="mega-cache-toast"></div>
|
|
433
|
+
<div class="achievement-tile" id="opie-tile"></div>
|
|
434
|
+
<h3 style="font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-top:16px">Achievements</h3>
|
|
435
|
+
<div id="ach-toast"></div>
|
|
436
|
+
<div id="ach-tiles" class="ach-tiles">loadingโฆ</div>
|
|
437
|
+
<div class="game-leaderboards">
|
|
438
|
+
<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>
|
|
439
|
+
<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>
|
|
440
|
+
<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>
|
|
441
|
+
<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>
|
|
442
|
+
</div>
|
|
443
|
+
<div id="game-empty">No scores yet โ run a session with game mode on.</div>
|
|
444
|
+
</div>
|
|
445
|
+
|
|
349
446
|
<script>
|
|
350
447
|
(function() {
|
|
351
448
|
var evBox = document.getElementById('events');
|
|
@@ -518,7 +615,11 @@ export function dashboardHtml(tierName: string): string {
|
|
|
518
615
|
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)
|
|
519
616
|
}
|
|
520
617
|
pollSnapshot();
|
|
618
|
+
renderGameScores();
|
|
619
|
+
renderAchievements();
|
|
521
620
|
setInterval(pollSnapshot, 2000);
|
|
621
|
+
setInterval(renderGameScores, 2000);
|
|
622
|
+
setInterval(renderAchievements, 2000);
|
|
522
623
|
|
|
523
624
|
// SSE for events
|
|
524
625
|
function connectSSE() {
|
|
@@ -733,9 +834,175 @@ export function dashboardHtml(tierName: string): string {
|
|
|
733
834
|
pollServers();
|
|
734
835
|
setInterval(pollServers, 5000);
|
|
735
836
|
|
|
837
|
+
// --- Game-mode settings strip (S32) -------------------------------------
|
|
838
|
+
// Polls GET /api/game-state and applies the row to the settings controls +
|
|
839
|
+
// the document theme. On any control change, PUTs a partial patch back. The
|
|
840
|
+
// dashboard server is a detached child with no MegaRuntime ref, so it reads /
|
|
841
|
+
// writes the game_state SQLite row directly; the in-process MegaRuntime picks
|
|
842
|
+
// up the change via its fs.watch cache-eviction watcher (S32).
|
|
843
|
+
var gmCheckbox = document.getElementById('set-game-mode');
|
|
844
|
+
var gmTheme = document.getElementById('set-theme');
|
|
845
|
+
var gmTui = document.getElementById('set-tui-mode');
|
|
846
|
+
|
|
847
|
+
function applyGameState(gs) {
|
|
848
|
+
if (!gs) return;
|
|
849
|
+
if (gs.theme) document.documentElement.dataset.theme = gs.theme;
|
|
850
|
+
if (gmCheckbox) gmCheckbox.checked = !!gs.game_mode_on;
|
|
851
|
+
if (gmTheme) gmTheme.value = gs.theme || 'transparent';
|
|
852
|
+
if (gmTui) gmTui.value = gs.tui_display_mode || 'full';
|
|
853
|
+
}
|
|
854
|
+
function pollGameState() {
|
|
855
|
+
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)
|
|
856
|
+
}
|
|
857
|
+
function putGameState(patch) {
|
|
858
|
+
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)
|
|
859
|
+
}
|
|
860
|
+
if (gmCheckbox) gmCheckbox.addEventListener('change', function() { putGameState({ game_mode_on: gmCheckbox.checked }); });
|
|
861
|
+
if (gmTheme) gmTheme.addEventListener('change', function() { putGameState({ theme: gmTheme.value }); });
|
|
862
|
+
if (gmTui) gmTui.addEventListener('change', function() { putGameState({ tui_display_mode: gmTui.value }); });
|
|
863
|
+
pollGameState();
|
|
864
|
+
setInterval(pollGameState, 5000);
|
|
865
|
+
|
|
866
|
+
// --- Game Mode leaderboards (S34) ----------------------------------------
|
|
867
|
+
// Polls GET /api/game-scores per metric, renders the per-repo leaderboard
|
|
868
|
+
// tables, the MEGA CACHE banner, the hidden Opie's Wild Ride unlock tile, and
|
|
869
|
+
// the transient oopsie toast (fires when a NEW mega_cache trophy row appears
|
|
870
|
+
// since the last poll). Browser-side fetch only (PREVENT-PI-004).
|
|
871
|
+
var GAME_METRICS = ['cache', 'dedupe', 'turns', 'mega_cache'];
|
|
872
|
+
var GAME_EMPTY = true;
|
|
873
|
+
var lastMegaTs = 0;
|
|
874
|
+
var lastMaxLevel = 0;
|
|
875
|
+
function fmtPct(v) { return (Math.round(v * 10) / 10) + '%'; }
|
|
876
|
+
function fmtDate(ts) { return ts ? new Date(ts).toLocaleString() : 'โ'; }
|
|
877
|
+
function trophyMeta(m) { try { return (m && typeof m === 'object') ? m : {}; } catch (e) { return {}; } }
|
|
878
|
+
function renderGameScores() {
|
|
879
|
+
var results = {};
|
|
880
|
+
var pending = GAME_METRICS.length + 1; // metrics + repos badge
|
|
881
|
+
function done() {
|
|
882
|
+
if (--pending > 0) return;
|
|
883
|
+
var cache = results['cache'] || [];
|
|
884
|
+
if (cache.length) GAME_EMPTY = false;
|
|
885
|
+
document.getElementById('lb-cache').innerHTML = cache.map(function(r) {
|
|
886
|
+
return '<tr><td title="' + sanitize(r.repo_root) + '">' + sanitize(r.repo_root.split('/').pop()) + '</td><td class="num">' + sanitize(String(r.value)) + '</td></tr>';
|
|
887
|
+
}).join('') || '<tr><td colspan="2" class="repo-none">no data</td></tr>';
|
|
888
|
+
var dedupe = results['dedupe'] || [];
|
|
889
|
+
if (dedupe.length) GAME_EMPTY = false;
|
|
890
|
+
document.getElementById('lb-dedupe').innerHTML = dedupe.map(function(r) {
|
|
891
|
+
return '<tr><td title="' + sanitize(r.repo_root) + '">' + sanitize(r.repo_root.split('/').pop()) + '</td><td class="num">' + sanitize(String(r.value)) + '</td></tr>';
|
|
892
|
+
}).join('') || '<tr><td colspan="2" class="repo-none">no data</td></tr>';
|
|
893
|
+
var turns = results['turns'] || [];
|
|
894
|
+
var maxTurns = turns.reduce(function(mx, r) { return Math.max(mx, r.value); }, 0);
|
|
895
|
+
var lvl = Math.floor(Math.log2(maxTurns + 1)) + 1;
|
|
896
|
+
var lvlEl = document.getElementById('turns-level');
|
|
897
|
+
if (lvlEl) lvlEl.textContent = 'LVL ' + lvl;
|
|
898
|
+
if (turns.length) GAME_EMPTY = false;
|
|
899
|
+
// level-up pulse: when max level increases vs last poll, pulse the cache bar
|
|
900
|
+
if (lvl > lastMaxLevel && lastMaxLevel > 0) {
|
|
901
|
+
var bar = document.getElementById('ctx-bar');
|
|
902
|
+
if (bar) { bar.classList.add('level-up'); setTimeout(function() { bar.classList.remove('level-up'); }, 1200); }
|
|
903
|
+
}
|
|
904
|
+
if (lvl > lastMaxLevel) lastMaxLevel = lvl;
|
|
905
|
+
document.getElementById('lb-turns').innerHTML = turns.map(function(r) {
|
|
906
|
+
return '<tr><td title="' + sanitize(r.repo_root) + '">' + sanitize(r.repo_root.split('/').pop()) + '</td><td class="num">' + sanitize(String(r.value)) + '</td></tr>';
|
|
907
|
+
}).join('') || '<tr><td colspan="2" class="repo-none">no data</td></tr>';
|
|
908
|
+
// mega_cache trophies + banner + Opie tile + transient toast
|
|
909
|
+
var mega = results['mega_cache'] || [];
|
|
910
|
+
var megaBody = document.getElementById('lb-mega_cache');
|
|
911
|
+
if (megaBody) {
|
|
912
|
+
megaBody.innerHTML = mega.map(function(r) {
|
|
913
|
+
var m = trophyMeta(r.meta);
|
|
914
|
+
var fs = m.firstSeenTs || m.firstSeen || r.ts;
|
|
915
|
+
var extra = fs ? ' <span class="lb-meta">' + sanitize(fmtDate(fs)) + '</span>' : '';
|
|
916
|
+
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>';
|
|
917
|
+
}).join('') || '<tr><td colspan="2" class="repo-none">no trophies yet</td></tr>';
|
|
918
|
+
}
|
|
919
|
+
var banner = document.getElementById('mega-cache-banner');
|
|
920
|
+
var tile = document.getElementById('opie-tile');
|
|
921
|
+
var best = null, firstSeen = null;
|
|
922
|
+
mega.forEach(function(r) {
|
|
923
|
+
if (best == null || r.value > best) best = r.value;
|
|
924
|
+
var m = trophyMeta(r.meta);
|
|
925
|
+
var fs = m.firstSeenTs || m.firstSeen || r.ts;
|
|
926
|
+
if (firstSeen == null || fs < firstSeen) firstSeen = fs;
|
|
927
|
+
});
|
|
928
|
+
if (banner) {
|
|
929
|
+
if (best != null && best > 100) {
|
|
930
|
+
banner.style.display = 'block';
|
|
931
|
+
banner.textContent = '๐ฅง MEGA CACHE! peak ' + fmtPct(best) + ' โ first reached ' + fmtDate(firstSeen);
|
|
932
|
+
} else { banner.style.display = 'none'; }
|
|
933
|
+
}
|
|
934
|
+
if (tile) {
|
|
935
|
+
if (best != null && best > 100) {
|
|
936
|
+
tile.style.display = 'block';
|
|
937
|
+
tile.className = 'achievement-tile unlocked';
|
|
938
|
+
tile.innerHTML = '๐ Opie\'s Wild Ride<span class="ach-detail">best ' + sanitize(fmtPct(best)) + ' ยท first ' + sanitize(fmtDate(firstSeen)) + '</span>';
|
|
939
|
+
} else { tile.style.display = 'none'; tile.className = 'achievement-tile'; }
|
|
940
|
+
}
|
|
941
|
+
// transient oopsie toast: a NEW mega_cache trophy row since the last poll
|
|
942
|
+
var maxTs = mega.reduce(function(mx, r) { return Math.max(mx, r.ts); }, 0);
|
|
943
|
+
var newRow = mega.find(function(r) { return r.ts > lastMegaTs && r.value > 100; });
|
|
944
|
+
if (lastMegaTs && newRow) {
|
|
945
|
+
var toast = document.getElementById('mega-cache-toast');
|
|
946
|
+
if (toast) {
|
|
947
|
+
toast.textContent = 'oopsie! cache went to ' + Math.round(newRow.value) + '% โ MEGA CACHE ๐ฅง';
|
|
948
|
+
toast.classList.add('show');
|
|
949
|
+
setTimeout(function() { toast.classList.remove('show'); }, 4000);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (maxTs > lastMegaTs) lastMegaTs = maxTs;
|
|
953
|
+
// empty state
|
|
954
|
+
var emptyEl = document.getElementById('game-empty');
|
|
955
|
+
if (emptyEl) emptyEl.style.display = GAME_EMPTY ? 'block' : 'none';
|
|
956
|
+
}
|
|
957
|
+
GAME_METRICS.forEach(function(m) {
|
|
958
|
+
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);
|
|
959
|
+
});
|
|
960
|
+
fetch('/api/game-scores?metric=repos&limit=1').then(function(r) { return r.ok ? r.json() : []; }).then(function(rows) {
|
|
961
|
+
var badge = document.getElementById('repos-badge');
|
|
962
|
+
if (badge) badge.textContent = ((rows && rows.length) ? rows[0].value : 0) + ' repos';
|
|
963
|
+
if (rows && rows.length && rows[0].value > 0) GAME_EMPTY = false;
|
|
964
|
+
}).catch(function() {}).then(done);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// --- Achievements tile row (S35) ------------------------------------------
|
|
968
|
+
// Polls GET /api/achievements; renders the tile row (hidden+locked render
|
|
969
|
+
// NOTHING; unlocked show icon+title+date; visible-but-locked show ??? teaser)
|
|
970
|
+
// and fires a transient toast when a newly-unlocked achievement appears.
|
|
971
|
+
// Browser-side fetch only (PREVENT-PI-004).
|
|
972
|
+
var lastAchMaxTs = 0;
|
|
973
|
+
function renderAchievements() {
|
|
974
|
+
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)
|
|
975
|
+
var box = document.getElementById('ach-tiles');
|
|
976
|
+
if (!box) return;
|
|
977
|
+
var html = '';
|
|
978
|
+
var maxTs = 0;
|
|
979
|
+
(rows || []).forEach(function(a) {
|
|
980
|
+
if (a.hidden === 1 && a.unlocked_at == null) return; // hidden invariant: render nothing
|
|
981
|
+
if (a.unlocked_at != null) {
|
|
982
|
+
var isNew = a.unlocked_at > lastAchMaxTs && lastAchMaxTs > 0;
|
|
983
|
+
maxTs = Math.max(maxTs, a.unlocked_at);
|
|
984
|
+
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>';
|
|
985
|
+
} else {
|
|
986
|
+
html += '<div class="ach-tile locked">??? ' + sanitize(a.title) + '</div>';
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
box.innerHTML = html || '<span class="repo-none">no achievements yet</span>';
|
|
990
|
+
var newly = (rows || []).filter(function(a) { return a.unlocked_at != null && a.unlocked_at > lastAchMaxTs; });
|
|
991
|
+
if (lastAchMaxTs && newly.length) {
|
|
992
|
+
var toast = document.getElementById('ach-toast');
|
|
993
|
+
if (toast) {
|
|
994
|
+
toast.textContent = newly.map(function(a) { return (a.icon || '') + ' ' + a.title; }).join(', ') + ' unlocked!';
|
|
995
|
+
toast.classList.add('show');
|
|
996
|
+
setTimeout(function() { toast.classList.remove('show'); }, 4000);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (maxTs > lastAchMaxTs) lastAchMaxTs = maxTs;
|
|
1000
|
+
}).catch(function() {});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
736
1003
|
// --- Tab switching ------------------------------------------------------
|
|
737
1004
|
var tabs = document.querySelectorAll('.tab');
|
|
738
|
-
var panels = { current: 'panel-current', all: 'panel-all', active: 'panel-active', summary: 'panel-summary' };
|
|
1005
|
+
var panels = { current: 'panel-current', all: 'panel-all', active: 'panel-active', summary: 'panel-summary', game: 'panel-game' };
|
|
739
1006
|
for (var i = 0; i < tabs.length; i++) {
|
|
740
1007
|
tabs[i].addEventListener('click', function() {
|
|
741
1008
|
var name = this.getAttribute('data-tab');
|
|
@@ -749,6 +1016,7 @@ export function dashboardHtml(tierName: string): string {
|
|
|
749
1016
|
}
|
|
750
1017
|
if (name === 'all' || name === 'summary') pollIndex();
|
|
751
1018
|
if (name === 'active') pollServers();
|
|
1019
|
+
if (name === 'game') { renderGameScores(); renderAchievements(); }
|
|
752
1020
|
});
|
|
753
1021
|
}
|
|
754
1022
|
})();
|