homepage: Protoss rename + fused Model Information component
The model cards become the StarCraft trio: frank-large -> frank-dragoon, frank-primary -> frank-archon, frank-secondary -> frank-zealot (family names only; endpoint/API ids unchanged). The three cards fuse into one Model Information component: per-unit Protoss accent colours and hex- clipped unit art, 15px type, clickable StarCraft unit-voice quotations (original .ogg files from the fandom wiki, served from /sfx/), supervision notes (dragoon/archon safe for autonomous use, zealot tight supervision), compaction-model recommendation for frank-zealot, and uncensored-model legal notes rendered as hover tooltips instead of wall text. docs.html carries the same renames plus api-id mapping. App-card updates: Seerr, Calibre-Web, RomM, Jellyfin set to WIP: DOWN with Open links removed (new wip "down" badge state); Copyparty notes it serves all other media apps content including WIP apps; Gitea and Plex noted as needing logins separate from SSO; SearXNG noted as obfuscated, not anonymous (upstream API keys tie to Frank). Plumbing: Caddy @data allowlist grew /sfx/*; deploy_homepage copies sfx/; homepage_config.bats pins the new probe names, the component fields, art/voice-file presence, and the wip-down schema.
This commit is contained in:
1 parent
74da746c51
commit
203b0be145
19 files changed
+312
-88
No files matched your search
+83
-5
@@ -19,6 +19,7 @@ const BADGE_LABEL = {
|
||||
down: 'DOWN',
|
||||
'wip-open': 'WIP: OPEN',
|
||||
'wip-closed': 'WIP: CLOSED',
|
||||
'wip-down': 'WIP: DOWN',
|
||||
};
|
||||
|
||||
function load(url) {
|
||||
@@ -31,14 +32,13 @@ function esc(s) {
|
||||
});
|
||||
}
|
||||
|
||||
// The description: structured role/spec/use lines when the service defines
|
||||
// them (the LLM cards), else the plain description string.
|
||||
// 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 || d.equiv)) {
|
||||
if (d && (d.role || d.spec || d.inputs || d.use)) {
|
||||
const lines = [];
|
||||
if (d.role) lines.push('<div class="desc-role">' + esc(d.role) + '</div>');
|
||||
if (d.equiv) lines.push('<div class="desc-equiv">' + esc(d.equiv) + '</div>');
|
||||
if (d.spec) lines.push('<div class="desc-spec">' + esc(d.spec) + '</div>');
|
||||
if (d.inputs && d.inputs.length) lines.push('<div class="desc-inputs">Inputs: ' + esc(d.inputs.join(' · ')) + '</div>');
|
||||
if (d.use) lines.push('<div class="desc-use">Use: ' + esc(d.use) + '</div>');
|
||||
@@ -47,6 +47,77 @@ function descHTML(svc) {
|
||||
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 '<button type="button" class="mi-voice" title="play voice" ' +
|
||||
'data-sfx="' + esc(files.join(',')) + '">▶ ' + esc(text) + '</button>';
|
||||
}
|
||||
|
||||
function miCard(svc, state) {
|
||||
const m = svc.model;
|
||||
const parts = ['<div class="mi-top">' +
|
||||
'<img class="mi-art" src="/img/unit-' + esc(m.unit) + '.jpg" alt="' + esc(m.unit) + '">' +
|
||||
'<div class="mi-head"><div class="mi-id"><span class="badge ' + state + '">' + BADGE_LABEL[state] + '</span>' +
|
||||
'<span class="mi-name">' + esc(svc.name) + '</span></div>' +
|
||||
'<div class="mi-role">' + esc(m.role) + '</div></div></div>',
|
||||
voiceBtn(m.tagline, m.sfx)
|
||||
];
|
||||
if (m.equiv) parts.push('<div class="mi-equiv">' + esc(m.equiv) + '</div>');
|
||||
if (m.spec) parts.push('<div class="mi-spec">' + esc(m.spec) + '</div>');
|
||||
if (m.inputs && m.inputs.length) parts.push('<div class="mi-inputs">Inputs: ' + esc(m.inputs.join(' · ')) + '</div>');
|
||||
if (m.use) parts.push('<div class="mi-use">Use: ' + esc(m.use) + '</div>');
|
||||
if (m.voice) parts.push(voiceBtn(m.voice.text, m.voice.sfx));
|
||||
if (m.supervision) parts.push('<div class="mi-super">' + esc(m.supervision) + '</div>');
|
||||
if (m.warn) parts.push('<div class="mi-warn" title="' + esc(m.warn) + '">' +
|
||||
'⚠ ' + esc(m.warn.split(':')[0]) + ' \u2014 hover to read</div>');
|
||||
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';
|
||||
head.textContent = 'Model Information';
|
||||
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');
|
||||
@@ -88,15 +159,22 @@ function renderPanels(services, status) {
|
||||
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]) {
|
||||
if (bySection[title]) root.appendChild(renderSection(title, bySection[title]));
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user