91 lines
3.8 KiB
HTML
91 lines
3.8 KiB
HTML
<!doctype html>
|
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Admin — Newbury Exhibit</title></head>
|
|
<body><div id="app"></div>
|
|
<script type="module">
|
|
/* The admin home. Everything here is generated from the TOOLS registry in
|
|
* admin.js, so adding a page to that array is the whole job — no risk of a
|
|
* tool existing but being reachable from nowhere, which is how the A3 test rig
|
|
* spent its first weeks.
|
|
*
|
|
* The status chips are deliberately cheap: one parallel burst of GETs that the
|
|
* pages themselves already call. Anything that fails is simply left off rather
|
|
* than blocking the page — the point of this screen is to get you somewhere
|
|
* else quickly, so it must never sit there spinning. */
|
|
import { requireAuth, styles, nav, TOOLS, GROUPS, href } from './admin.js';
|
|
|
|
document.head.insertAdjacentHTML('beforeend', `<style>${styles}
|
|
.mode { display:flex; align-items:center; gap:9px; flex-wrap:wrap;
|
|
margin:0 0 20px; font-size:.84rem; opacity:.8; }
|
|
.hub .empty { font-size:.8rem; opacity:.5; }
|
|
</style>`);
|
|
|
|
const info = await requireAuth();
|
|
|
|
const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&':'&','<':'<','>':'>','"':'"' }[c]));
|
|
const n = v => Array.isArray(v) ? v.length : 0;
|
|
|
|
/* Never let one dead endpoint take the page down. */
|
|
const get = async (p) => {
|
|
try {
|
|
const r = await fetch(p, { headers: { 'x-exhibit-token': localStorage.getItem('exhibitToken') || '' } });
|
|
return r.ok ? await r.json() : null;
|
|
} catch { return null; }
|
|
};
|
|
|
|
const [scene, playlist, characters, errors, ghosts] = await Promise.all([
|
|
get('/api/scene'), get('/api/playlist'), get('/api/characters'),
|
|
get('/api/client-errors'), get('/api/ghosts'),
|
|
]);
|
|
|
|
/* id -> [{text, cls}]. Only facts worth knowing before you click. */
|
|
const chips = {};
|
|
const add = (id, text, cls = '') => { (chips[id] ||= []).push({ text, cls }); };
|
|
|
|
if (scene) {
|
|
add('layout', `${n(scene.anchors)} crest${n(scene.anchors) === 1 ? '' : 's'}`);
|
|
if (n(scene.spawns)) add('layout', `${n(scene.spawns)} spawns`);
|
|
if (!n(scene.anchors)) add('layout', 'no crests yet', 'warn');
|
|
add('print', `${n(scene.anchors)} to print`);
|
|
}
|
|
if (playlist) {
|
|
const tracks = n(playlist.tracks), res = n(playlist.residents);
|
|
add('playlist', `${tracks} track${tracks === 1 ? '' : 's'}`);
|
|
if (res) add('playlist', `${res} resident${res === 1 ? '' : 's'}`);
|
|
if (n(playlist.windows)) add('playlist', `${n(playlist.windows)} time window${n(playlist.windows) === 1 ? '' : 's'}`);
|
|
}
|
|
if (characters) {
|
|
const c = Array.isArray(characters) ? characters.length : n(characters.characters);
|
|
if (c) add('characters', `${c} built`);
|
|
}
|
|
if (ghosts) {
|
|
const g = Array.isArray(ghosts) ? ghosts.length : n(ghosts.ghosts);
|
|
if (g) add('characters', `${g} in roster`);
|
|
}
|
|
if (Array.isArray(errors)) {
|
|
add('errors', errors.length ? `${errors.length} logged` : 'none', errors.length ? 'warn' : 'ok');
|
|
}
|
|
|
|
const card = t => `<a class="card" href="${href(t)}">
|
|
<p class="t">${esc(t.name)}${(chips[t.id] || []).map(c => `<span class="chip ${c.cls}">${esc(c.text)}</span>`).join('')}</p>
|
|
<p class="b">${esc(t.blurb)}</p>
|
|
</a>`;
|
|
|
|
const section = g => {
|
|
const ts = TOOLS.filter(t => t.group === g);
|
|
if (!ts.length) return '';
|
|
return `<section><h2>${esc(g)}</h2><div class="cards">${ts.map(card).join('')}</div></section>`;
|
|
};
|
|
|
|
const modeChip = info && info.mode === 'final'
|
|
? '<span class="chip ok">final — admin password protected</span>'
|
|
: '<span class="chip warn">dev mode — admin is open, no password</span>';
|
|
|
|
document.getElementById('app').innerHTML = `${nav('')}
|
|
<div class="hub">
|
|
<div class="mode">${modeChip}<span>Changes save to the live volume and push to viewers immediately.</span></div>
|
|
${GROUPS.map(section).join('')}
|
|
</div>`;
|
|
</script>
|
|
</body></html>
|