nostalgia integration
All checks were successful
Zone / build (push) Successful in 11s

This commit is contained in:
2026-06-26 12:36:00 +01:00
parent d4f24eea6a
commit 9e182e01d6
3 changed files with 152 additions and 39 deletions

View File

@@ -102,6 +102,58 @@
}, },
{
"id": "org-web-build",
"type": "buildStatus",
"title": "Website build",
"integration": "builds",
"lastRunKey": "org_web"
}
],
"links": [
{ "id": "timesheet", "label": "Timesheet", "href": "/time-tracking/", "group": "platform" },
{ "id": "website", "label": "Website", "href": "https://zainezq.com", "group": "platform", "external": true },
{ "id": "author", "label": "Author", "href": "https://author.zainezq.com", "group": "platform", "external": true },
{ "id": "files", "label": "Files", "href": "https://filebrowser.zainezq.com", "group": "infra", "external": true },
{ "id": "library", "label": "Library", "href": "https://calibre.zainezq.com", "group": "infra", "external": true },
{ "id": "news", "label": "News", "href": "https://miniflux.zainezq.com", "group": "infra", "external": true },
{ "id": "rdp", "label": "RDP", "href": "https://guac.zainezq.com", "group": "infra", "external": true },
{ "id": "portainer", "label": "Portainer", "href": "https://portainer.zainezq.com", "group": "infra", "external": true },
{ "id": "nextcloud", "label": "Nextcloud", "href": "https://nextcloud.zainezq.com", "group": "infra", "external": true },
{ "id": "pgadmin", "label": "Databases", "href": "https://pgadmin.zainezq.com", "group": "infra", "external": true },
{ "id": "gitea", "label": "Gitea", "href": "https://gitea.zainezq.com", "group": "infra", "external": true }
],
"builds": [
{
"id": "org_web",
"label": "Website Build",
"badge": "Web",
"confirmTitle": "Update Website", "confirmTitle": "Update Website",
"confirmBody": "This will rebuild and deploy the website. Continue?", "confirmBody": "This will rebuild and deploy the website. Continue?",

View File

@@ -253,6 +253,39 @@ function setBadge(el, text, tone) {
el.className = `badge ${tone} widget-badge`; 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) { function renderBuilds(builds) {
const grid = document.getElementById('buildGrid'); const grid = document.getElementById('buildGrid');
grid.innerHTML = ''; grid.innerHTML = '';
@@ -274,11 +307,7 @@ function createBuildCard(build) {
article.dataset.buildId = build.id; article.dataset.buildId = build.id;
article.setAttribute('data-build-id', build.id); article.setAttribute('data-build-id', build.id);
const btnId = build.id === 'org_web' ? 'buildBtn' : 'emacsBtn'; const { btnId, killId, resultId, logId, lastRunId } = buildDomIds(build);
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.innerHTML = ` article.innerHTML = `
<div class="panel-header"> <div class="panel-header">
@@ -388,8 +417,7 @@ async function fetchLastRuns() {
const res = await fetch(manifest.status.lastRuns); const res = await fetch(manifest.status.lastRuns);
const data = await res.json(); const data = await res.json();
for (const build of manifest.builds) { for (const build of manifest.builds) {
const metaId = build.id === 'org_web' ? 'webLastRunMeta' : 'emacsLastRunMeta'; renderLastRun(buildDomIds(build).lastRunId, data[build.lastRunKey]);
renderLastRun(metaId, data[build.lastRunKey]);
updateBuildStatusFromLastRun(build, data[build.lastRunKey]); updateBuildStatusFromLastRun(build, data[build.lastRunKey]);
} }
} catch { /* ignore */ } } catch { /* ignore */ }

View File

@@ -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.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 === 'org_web'));
assert.ok(manifest.builds.some(b => b.id === 'emacs')); 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', () => { 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.start.path, '/api/rerun-emacs');
assert.equal(build.lastRunKey, '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/build-web/status',
'/api/rerun-emacs', '/api/rerun-emacs',
'/api/rerun-emacs/logs', '/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) { 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, /data\/manifest\.json/);
assert.match(dashboardJs, /\/api\/zone\/status/); assert.match(dashboardJs, /\/api\/zone\/status/);
assert.match(dashboardJs, /renderWidgets/); assert.match(dashboardJs, /renderWidgets/);
assert.match(dashboardJs, /buildDomIds/);
assert.doesNotMatch(dashboardJs, /build-roam/); assert.doesNotMatch(dashboardJs, /build-roam/);
assert.doesNotMatch(dashboardJs, /run-combined/); assert.doesNotMatch(dashboardJs, /run-combined/);
}); });