From 9e182e01d68e827aa0e21e93c205e45db1f36dba Mon Sep 17 00:00:00 2001 From: Zaine Date: Fri, 26 Jun 2026 12:36:00 +0100 Subject: [PATCH] nostalgia integration --- data/manifest.json | 70 ++++++++++++++++++++++++++++----- js/dashboard.js | 86 +++++++++++++++++++++++++++-------------- test/dashboard.test.mjs | 35 ++++++++++++++++- 3 files changed, 152 insertions(+), 39 deletions(-) diff --git a/data/manifest.json b/data/manifest.json index faa9b0b..0481efb 100755 --- a/data/manifest.json +++ b/data/manifest.json @@ -87,10 +87,10 @@ "statusTextId": "webStatus", "lastRunKey": "org_web" }, - { - "id": "emacs", - "label": "Emacs", - "badge": "Emacs", + { + "id": "emacs", + "label": "Emacs", + "badge": "Emacs", "confirmTitle": "Rerun Emacs", "confirmBody": "This will rerun Emacs. Continue?", "runLabel": "Rerun Emacs", @@ -99,11 +99,63 @@ "status": { "path": "/api/rerun-emacs/status" }, "logs": { "path": "/api/rerun-emacs/logs" }, "statusDotId": "emacsDot", - "statusTextId": "emacsStatus", - "lastRunKey": "emacs", - "successExitCodes": [0, 124] - } - ], + "statusTextId": "emacsStatus", + "lastRunKey": "emacs", + "successExitCodes": [0, 124] + }, + { + "id": "adventure_resources", + "label": "AdventureOS Resources", + "badge": "AI", + "confirmTitle": "Free AdventureOS Resources", + "confirmBody": "This will run the configured resource cleanup command. Continue?", + "runLabel": "Free resources", + "start": { "method": "POST", "path": "/api/adventure/resources/free" }, + "cancel": { "method": "DELETE", "path": "/api/adventure/resources/free" }, + "status": { "path": "/api/adventure/resources/free/status" }, + "logs": { "path": "/api/adventure/resources/free/logs" }, + "lastRunKey": "adventure_resources" + }, + { + "id": "guacamole_start", + "label": "Guacamole Start", + "badge": "RDP", + "confirmTitle": "Start Guacamole", + "confirmBody": "This will start the configured Guacamole container. Continue?", + "runLabel": "Start RDP", + "start": { "method": "POST", "path": "/api/guacamole/start" }, + "cancel": { "method": "DELETE", "path": "/api/guacamole/start" }, + "status": { "path": "/api/guacamole/start/status" }, + "logs": { "path": "/api/guacamole/start/logs" }, + "lastRunKey": "guacamole_start" + }, + { + "id": "guacamole_stop", + "label": "Guacamole Stop", + "badge": "RDP", + "confirmTitle": "Stop Guacamole", + "confirmBody": "This will stop the configured Guacamole container. Continue?", + "runLabel": "Stop RDP", + "start": { "method": "POST", "path": "/api/guacamole/stop" }, + "cancel": { "method": "DELETE", "path": "/api/guacamole/stop" }, + "status": { "path": "/api/guacamole/stop/status" }, + "logs": { "path": "/api/guacamole/stop/logs" }, + "lastRunKey": "guacamole_stop" + }, + { + "id": "nostalgia_prod", + "label": "Nostalgia Production", + "badge": "Prod", + "confirmTitle": "Run Nostalgia Production", + "confirmBody": "This will run docker compose up -d --build in the Nostalgia directory. Continue?", + "runLabel": "Run production", + "start": { "method": "POST", "path": "/api/nostalgia/production" }, + "cancel": { "method": "DELETE", "path": "/api/nostalgia/production" }, + "status": { "path": "/api/nostalgia/production/status" }, + "logs": { "path": "/api/nostalgia/production/logs" }, + "lastRunKey": "nostalgia_prod" + } + ], "status": { "health": "/api/health", "uptime": "/api/uptime", diff --git a/js/dashboard.js b/js/dashboard.js index f0e251b..8720d53 100755 --- a/js/dashboard.js +++ b/js/dashboard.js @@ -247,15 +247,48 @@ function updateWidgetCard(widget, status) { } } -function setBadge(el, text, tone) { - if (!el) return; - el.textContent = text; - el.className = `badge ${tone} widget-badge`; -} - -function renderBuilds(builds) { - const grid = document.getElementById('buildGrid'); - grid.innerHTML = ''; +function setBadge(el, text, tone) { + if (!el) return; + el.textContent = text; + el.className = `badge ${tone} widget-badge`; +} + +function safeDomId(value) { + return String(value).replace(/[^a-zA-Z0-9_-]/g, '-'); +} + +function buildDomIds(build) { + if (build.id === 'org_web') { + return { + btnId: 'buildBtn', + killId: 'killWebBtn', + resultId: 'buildResult', + logId: 'logOutput', + lastRunId: 'webLastRunMeta', + }; + } + if (build.id === 'emacs') { + return { + btnId: 'emacsBtn', + killId: 'killEmacsBtn', + resultId: 'emacsResult', + logId: 'emacsLogs', + lastRunId: 'emacsLastRunMeta', + }; + } + const id = safeDomId(build.id); + return { + btnId: `${id}Btn`, + killId: `${id}CancelBtn`, + resultId: `${id}Result`, + logId: `${id}Logs`, + lastRunId: `${id}LastRunMeta`, + }; +} + +function renderBuilds(builds) { + const grid = document.getElementById('buildGrid'); + grid.innerHTML = ''; for (const build of builds) { grid.appendChild(createBuildCard(build)); } @@ -270,15 +303,11 @@ function renderBuilds(builds) { function createBuildCard(build) { const article = document.createElement('article'); - article.className = 'panel run-card'; - article.dataset.buildId = build.id; - article.setAttribute('data-build-id', build.id); - - const btnId = build.id === 'org_web' ? 'buildBtn' : 'emacsBtn'; - const killId = build.id === 'org_web' ? 'killWebBtn' : 'killEmacsBtn'; - const resultId = build.id === 'org_web' ? 'buildResult' : 'emacsResult'; - const logId = build.id === 'org_web' ? 'logOutput' : 'emacsLogs'; - const lastRunId = build.id === 'org_web' ? 'webLastRunMeta' : 'emacsLastRunMeta'; + article.className = 'panel run-card'; + article.dataset.buildId = build.id; + article.setAttribute('data-build-id', build.id); + + const { btnId, killId, resultId, logId, lastRunId } = buildDomIds(build); article.innerHTML = `
@@ -383,17 +412,16 @@ async function fetchUptime() { } catch { /* ignore */ } } -async function fetchLastRuns() { - try { - const res = await fetch(manifest.status.lastRuns); - const data = await res.json(); - for (const build of manifest.builds) { - const metaId = build.id === 'org_web' ? 'webLastRunMeta' : 'emacsLastRunMeta'; - renderLastRun(metaId, data[build.lastRunKey]); - updateBuildStatusFromLastRun(build, data[build.lastRunKey]); - } - } catch { /* ignore */ } -} +async function fetchLastRuns() { + try { + const res = await fetch(manifest.status.lastRuns); + const data = await res.json(); + for (const build of manifest.builds) { + renderLastRun(buildDomIds(build).lastRunId, data[build.lastRunKey]); + updateBuildStatusFromLastRun(build, data[build.lastRunKey]); + } + } catch { /* ignore */ } +} function updateBuildStatusFromLastRun(build, info) { if (!build.statusDotId || !info || info.lastRun === 'never') return; diff --git a/test/dashboard.test.mjs b/test/dashboard.test.mjs index 96b6f96..be44b5a 100755 --- a/test/dashboard.test.mjs +++ b/test/dashboard.test.mjs @@ -74,6 +74,10 @@ test('manifest defines unique link and build ids', () => { assert.ok(manifest.links.some(l => l.id === 'timesheet' && l.href === '/time-tracking/')); assert.ok(manifest.builds.some(b => b.id === 'org_web')); assert.ok(manifest.builds.some(b => b.id === 'emacs')); + assert.ok(manifest.builds.some(b => b.id === 'adventure_resources')); + assert.ok(manifest.builds.some(b => b.id === 'guacamole_start')); + assert.ok(manifest.builds.some(b => b.id === 'guacamole_stop')); + assert.ok(manifest.builds.some(b => b.id === 'nostalgia_prod')); }); test('manifest build cards expose stable dom ids', () => { @@ -88,6 +92,22 @@ test('manifest build cards expose stable dom ids', () => { assert.equal(build.start.path, '/api/rerun-emacs'); assert.equal(build.lastRunKey, 'emacs'); } + if (build.id === 'adventure_resources') { + assert.equal(build.start.path, '/api/adventure/resources/free'); + assert.equal(build.lastRunKey, 'adventure_resources'); + } + if (build.id === 'guacamole_start') { + assert.equal(build.start.path, '/api/guacamole/start'); + assert.equal(build.lastRunKey, 'guacamole_start'); + } + if (build.id === 'guacamole_stop') { + assert.equal(build.start.path, '/api/guacamole/stop'); + assert.equal(build.lastRunKey, 'guacamole_stop'); + } + if (build.id === 'nostalgia_prod') { + assert.equal(build.start.path, '/api/nostalgia/production'); + assert.equal(build.lastRunKey, 'nostalgia_prod'); + } } }); @@ -101,7 +121,19 @@ test('dashboard module references manifest-driven API endpoints', () => { '/api/build-web/status', '/api/rerun-emacs', '/api/rerun-emacs/logs', - '/api/rerun-emacs/status' + '/api/rerun-emacs/status', + '/api/adventure/resources/free', + '/api/adventure/resources/free/logs', + '/api/adventure/resources/free/status', + '/api/guacamole/start', + '/api/guacamole/start/logs', + '/api/guacamole/start/status', + '/api/guacamole/stop', + '/api/guacamole/stop/logs', + '/api/guacamole/stop/status', + '/api/nostalgia/production', + '/api/nostalgia/production/logs', + '/api/nostalgia/production/status' ]; for (const endpoint of expectedEndpoints) { @@ -123,6 +155,7 @@ test('dashboard module references manifest-driven API endpoints', () => { assert.match(dashboardJs, /data\/manifest\.json/); assert.match(dashboardJs, /\/api\/zone\/status/); assert.match(dashboardJs, /renderWidgets/); + assert.match(dashboardJs, /buildDomIds/); assert.doesNotMatch(dashboardJs, /build-roam/); assert.doesNotMatch(dashboardJs, /run-combined/); });