// The homepage front end. It reads status.json and services.json once on load // (no auto-refresh, Q41), renders the services for this page's category as // sectioned grids of TUI panels, offers the styling toggle (Q27), and shows // the last master deployment datetime (Q43). It never alters the user's // cursor (Q28). const CATEGORY = window.location.pathname.indexOf('/admin') !== -1 ? 'admin' : 'user'; // Sections and panels are shared components: both pages render through the // same renderSection()/renderPanel() below. SECTION_ORDER fixes each page's // section order; a section with no services renders nothing. const SECTION_ORDER = { user: ['Web Apps', 'Media Library Apps', 'AI'], admin: ['Media Library Apps', 'Observability', 'AI', 'Infrastructure'], }; const BADGE_LABEL = { up: 'UP', down: 'DOWN', 'wip-open': 'WIP: OPEN', 'wip-closed': 'WIP: CLOSED', 'wip-down': 'WIP: DOWN', }; function load(url) { return fetch(url, { cache: 'no-store' }).then(function (r) { return r.json(); }); } function esc(s) { return String(s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } // The plain description for ordinary panels. (Model cards render through // renderModelInfo below, not through descHTML.) function descHTML(svc) { const d = svc.details; if (d && (d.role || d.spec || d.inputs || d.use)) { const lines = []; if (d.role) lines.push('
' + esc(d.role) + '
'); if (d.spec) lines.push('
' + esc(d.spec) + '
'); if (d.inputs && d.inputs.length) lines.push('
Inputs: ' + esc(d.inputs.join(' · ')) + '
'); if (d.use) lines.push('
Use: ' + esc(d.use) + '
'); return lines.join(''); } return esc(svc.description || ''); } // The Model Information component: one panel fusing every fleet model card. // Each model is an informational sub-card (smaller type, Protoss unit art, // unit-coloured flair); the quoted lines are buttons that play the matching // StarCraft unit voice from /sfx/. Audio files are sequential: a click plays // the whole queue (the Dragoon tagline is two lines spliced). let sfxQueue = []; let sfxPlaying = false; function playSfx(files, el) { sfxQueue = sfxQueue.concat(files); if (sfxPlaying) return; sfxPlaying = true; el.classList.add('mi-speaking'); const next = function () { const f = sfxQueue.shift(); if (!f) { sfxPlaying = false; el.classList.remove('mi-speaking'); return; } const a = new Audio('/sfx/' + f); a.volume = 0.5; a.onended = next; a.onerror = next; a.play().then(function () {}, next); }; next(); } function voiceBtn(text, files) { return ''; } function miCard(svc, state) { const m = svc.model; const parts = ['
' + '' + esc(m.unit) + '' + '
' + BADGE_LABEL[state] + '' + '' + esc(svc.name) + '
' + '
' + esc(m.role) + '
' + (m.hf ? 'model on huggingface ↗' : '') + '
', voiceBtn(m.tagline, m.sfx) ]; if (m.equiv) parts.push('
' + esc(m.equiv) + '
'); if (m.spec) parts.push('
' + esc(m.spec) + '
'); if (m.inputs && m.inputs.length) parts.push('
Inputs: ' + esc(m.inputs.join(' · ')) + '
'); if (m.use) parts.push('
Use: ' + esc(m.use) + '
'); if (m.compaction) parts.push('
⚡ ' + esc(m.compaction) + '
'); if (m.supervision) parts.push('
' + esc(m.supervision) + '
'); if (m.permission) parts.push('
⚠ ' + esc(m.permission) + '
'); if (m.warn) parts.push('
⚠ privacy note' + '
' + esc(m.warn) + '
'); const card = document.createElement('article'); card.className = 'mi-card mi-' + esc(m.unit); card.innerHTML = parts.join(''); return card; } function renderModelInfo(models) { const panel = document.createElement('div'); panel.className = 'panel model-info'; const head = document.createElement('div'); head.className = 'mi-title'; const title = document.createElement('span'); title.textContent = 'Model Information'; head.appendChild(title); const docsLink = document.createElement('a'); docsLink.className = 'mi-docs-link'; docsLink.href = '/docs#models'; docsLink.textContent = 'AI guide \u2192'; head.appendChild(docsLink); panel.appendChild(head); const grid = document.createElement('div'); grid.className = 'mi-grid'; for (const m of models) grid.appendChild(m.el); panel.appendChild(grid); panel.addEventListener('click', function (ev) { const btn = ev.target.closest('.mi-voice'); if (btn) playSfx(btn.getAttribute('data-sfx').split(','), btn); }); return panel; } // Shared panel component: one TUI panel for a service in a given state. function renderPanel(svc, state) { const panel = document.createElement('div'); panel.className = 'panel ' + state; const links = []; if (svc.showLink && svc.url) links.push('open'); if (svc.docs) links.push('docs'); const art = svc.art ? '' : ''; const warn = svc.warn ? '
⚠ privacy note' + '
' + esc(svc.warn) + '
' : ''; panel.innerHTML = '
' + art + ' ' + BADGE_LABEL[state] + '' + ' ' + esc(svc.name) + '' + '
' + '
' + descHTML(svc) + '
' + '' + warn; if (svc.showLink && svc.url) { panel.classList.add('clickable'); panel.addEventListener('click', function (ev) { if (ev.target.closest('a, .svc-warn')) return; }); } return panel; } // Shared section component: a heading over a grid of panels. function renderSection(title, panels) { const section = document.createElement('section'); section.className = 'section'; const heading = document.createElement('h2'); heading.className = 'section-title'; heading.textContent = title; const grid = document.createElement('div'); grid.className = 'grid'; for (const panel of panels) grid.appendChild(panel); section.appendChild(heading); section.appendChild(grid); return section; } function renderPanels(services, status) { const stateByName = {}; for (const s of (status.services || [])) stateByName[s.name] = s.state; const root = document.getElementById('grid'); root.innerHTML = ''; let up = 0, down = 0, wip = 0; const bySection = {}; const modelsBySection = {}; for (const svc of services) { if (svc.category !== CATEGORY) continue; const state = svc.wip ? 'wip-' + svc.wip : (stateByName[svc.name] || 'down'); if (state === 'up') up++; else if (state === 'down') down++; else wip++; if (svc.model) { (modelsBySection[svc.section] = modelsBySection[svc.section] || []).push({ svc: svc, el: miCard(svc, state) }); continue; } (bySection[svc.section] = bySection[svc.section] || []).push(renderPanel(svc, state)); } for (const title of SECTION_ORDER[CATEGORY]) { const panels = bySection[title] || []; if (modelsBySection[title]) panels.push(renderModelInfo(modelsBySection[title])); if (panels.length) root.appendChild(renderSection(title, panels)); } document.getElementById('summary').textContent = 'UP ' + up + ' DOWN ' + down + ' WIP ' + wip; } // The styling toggle (Q27): ?bare=1 strips the page to bare, unstyled HTML. function applyBare() { const bare = new URLSearchParams(window.location.search).get('bare') === '1'; document.documentElement.setAttribute('data-bare', bare ? '1' : '0'); const t = document.getElementById('bare-toggle'); if (t) { t.textContent = bare ? 'styled' : 'bare'; t.href = bare ? '?bare=0' : '?bare=1'; } } // The last master deployment datetime (Q43), baked in at deploy time. function fillDeployed() { const el = document.getElementById('deployed-at'); if (el && window.DEPLOYED_AT) el.textContent = window.DEPLOYED_AT; } // Show the admin button only to members of the authentik homepage-admins // group (the same group the /admin* server gate enforces). The data comes // from Caddy's /whoami, which echoes the outpost-injected identity headers. function gateAdminLink() { const link = document.getElementById('admin-link'); if (!link) return; fetch('/whoami', { headers: { accept: 'application/json' } }) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (me) { if (me && /homepage-admins/i.test(me.groups || '')) link.hidden = false; }) .catch(function () {}); } function main() { applyBare(); fillDeployed(); gateAdminLink(); Promise.all([load('services.json'), load('status.json')]) .then(function (pair) { renderPanels(pair[0], pair[1]); }) .catch(function (e) { document.getElementById('summary').textContent = 'status unavailable: ' + e; }); } main();