// 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 = ['