Add isolated read-only spectator monitor for public sharing

This commit is contained in:
Codex
2026-09-22 19:50:05 -04:00
parent 8e96c7ff47
commit 7e19d82a02
7 changed files with 379 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
'use strict';
const $ = id => document.getElementById(id);
let realm;
function el(tag, text, cls) { const node = document.createElement(tag); if (text !== undefined) node.textContent = String(text); if (cls) node.className = cls; return node; }
function empty(node, text) { node.replaceChildren(el('p', text, 'empty')); }
function renderRoster() {
if (!realm) return;
const term = $('search').value.trim().toLowerCase(), faction = $('faction').value;
const bots = realm.bots.filter(b => (faction === 'all' || b.faction === faction) && [b.name,b.class,b.race,b.zone,b.guild].join(' ').toLowerCase().includes(term));
$('roster').replaceChildren(...bots.map(b => {
const row = el('tr'); row.append(el('td', b.name));
const team = el('td'); team.append(el('span', b.faction, 'tag ' + b.faction.toLowerCase())); row.append(team);
const role = el('td', b.class); role.append(el('small', b.race)); row.append(role, el('td', b.level), el('td', b.zone || 'Unknown'), el('td', b.guild || '—')); return row;
}));
$('roster-empty').hidden = bots.length > 0;
$('roster-empty').textContent = realm.bots.length ? 'No residents match this search.' : 'No residents are currently reported online.';
}
function renderFeed() {
if (!realm) return;
const entries = realm.feed.filter(e => $('activity-filter').value === 'all' || e.kind === $('activity-filter').value);
if (!entries.length) return empty($('feed'), 'No confirmed activity of this type yet. New encounters will appear here.');
$('feed').replaceChildren(...entries.map(e => {
const node = el('article', undefined, 'entry');
node.append(el('span', e.kind + ' · ' + new Date(e.at).toLocaleString(), 'meta'), el('h3', e.names.join(' ↔ ')), el('p', e.text)); return node;
}));
}
function render() {
$('total').textContent = realm.bots.length;
$('alliance').textContent = realm.bots.filter(b => b.faction === 'Alliance').length;
$('horde').textContent = realm.bots.filter(b => b.faction === 'Horde').length;
$('parties-count').textContent = realm.parties.length;
$('status').className = 'status' + (realm.stale ? ' stale' : '');
$('status').textContent = (realm.stale ? 'Updates delayed · Showing last known roster' : 'Live realm observations') + (realm.rosterAt ? ' · Observed ' + new Date(realm.rosterAt * 1000).toLocaleTimeString() : '');
renderRoster(); renderFeed();
$('parties').replaceChildren(...realm.parties.map(p => { const n=el('article',undefined,'group');n.append(el('strong',p.members.join(' · ')),el('p',p.faction));return n; }));
if (!realm.parties.length) empty($('parties'), 'No bot parties observed yet.');
const goals={quest:'Complete quests together',support:'Help guildmates in combat',explore:'Explore other maps'};
$('guilds').replaceChildren(...realm.guilds.map(g => { const n=el('article',undefined,'group');n.append(el('strong',g.name),el('p',goals[g.goal]||'Shared adventures'),el('p',g.progress+' contributions · milestone '+g.target));const p=el('progress');p.max=Math.max(1,g.target);p.value=g.progress;p.setAttribute('aria-label',g.name+' goal progress');n.append(p);return n; }));
if (!realm.guilds.length) empty($('guilds'), 'Waiting for guild observations.');
}
async function refresh() {
try { const response=await fetch('/api/realm',{cache:'no-store',signal:AbortSignal.timeout(10000)});if(!response.ok)throw new Error();realm=await response.json();render(); }
catch { $('status').className='status stale';$('status').textContent=realm?'Connection interrupted · Showing the last observation. Retrying automatically.':'Realm temporarily unavailable. Retrying automatically.'; }
finally { setTimeout(refresh,15000); }
}
$('search').addEventListener('input',renderRoster);$('faction').addEventListener('change',renderRoster);$('activity-filter').addEventListener('change',renderFeed);refresh();
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark"><title>AzerothAI · Living Realm</title><link rel="stylesheet" href="/style.css"><script src="/app.js" defer></script></head>
<body><main>
<header><div><p class="eyebrow">AZEROTHAI / SPECTATOR</p><h1>A world with a life of its own.</h1><p class="intro">Follow the residents of Azeroth as they adventure, meet, and build a shared history.</p></div><span class="badge">View only</span></header>
<div class="status" role="status" id="status">Connecting to the realm…</div>
<section class="stats" aria-label="Realm population"><article><span>Residents online</span><strong id="total">—</strong><small>50 resident limit</small></article><article class="alliance"><span>Alliance</span><strong id="alliance">—</strong><small>25 resident target</small></article><article class="horde"><span>Horde</span><strong id="horde">—</strong><small>25 resident target</small></article><article><span>Adventuring parties</span><strong id="parties-count">—</strong><small>Observed bot groups</small></article></section>
<nav aria-label="Realm sections"><a href="#residents">Residents</a><a href="#activity">Social life</a><a href="#groups">Parties & guilds</a></nav>
<section id="residents" class="panel"><div class="heading"><div><p class="eyebrow">IN THE WORLD</p><h2>Meet the residents</h2></div><div class="filters"><label><span class="sr-only">Search residents</span><input id="search" type="search" placeholder="Find a name, class, or zone…"></label><label><span class="sr-only">Faction</span><select id="faction"><option value="all">Both factions</option><option>Alliance</option><option>Horde</option></select></label></div></div><div class="table-wrap"><table><thead><tr><th>Name</th><th>Faction</th><th>Class / race</th><th>Level</th><th>Location</th><th>Guild</th></tr></thead><tbody id="roster"></tbody></table></div><p id="roster-empty" class="empty">Waiting for the first observation.</p></section>
<div class="columns"><section id="activity" class="panel"><div class="heading"><div><p class="eyebrow">SHARED STORIES</p><h2>Social life</h2></div><label><span class="sr-only">Activity type</span><select id="activity-filter"><option value="all">All activity</option><option value="conversation">Conversations</option><option value="relationship">Relationships</option></select></label></div><div id="feed" class="feed"></div></section><section id="groups" class="panel"><div class="heading"><div><p class="eyebrow">BETTER TOGETHER</p><h2>Parties & guilds</h2></div></div><div class="group-body"><h3>Current parties</h3><div id="parties"></div><h3 class="guild-title">Guild ambitions</h3><div id="guilds"></div></div></section></div>
<footer>Automatic updates every 15 seconds · Population can briefly dip while a replacement joins.<br>Bot-to-bot conversations only. Player whispers stay private.</footer>
</main></body></html>
+1
View File
@@ -0,0 +1 @@
:root{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#e6e8ed;background:#0b1017;font-synthesis:none}*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse at 15% 0,#23314455,transparent 55%)}main{max-width:1280px;margin:auto;padding:48px 28px}header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start}h1{font-family:Georgia,serif;font-size:clamp(30px,4vw,48px);font-weight:400;letter-spacing:-1px;margin:12px 0}h2{font-size:22px;margin:5px 0}h3{font-size:15px;margin:0 0 14px}.eyebrow{font-size:11px;letter-spacing:2px;color:#c3a56f;margin:0}.intro{max-width:640px;color:#a9b2c1;line-height:1.7}.badge{border:1px solid #415245;border-radius:30px;color:#a8c8a5;white-space:nowrap;padding:8px 14px;font-size:12px}.status{color:#a9b2c1;margin:20px 0;font-size:13px}.status.stale{color:#f2bd79}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}.stats article{background:#121a25;border:1px solid #293241;border-radius:14px;padding:22px;display:flex;flex-direction:column;gap:6px}.stats span{font-size:13px;color:#bec7d3}.stats strong{font-size:40px;font-weight:500;color:#dcc18e}.stats small{font-size:11px;color:#8e9aaa}.stats .alliance strong{color:#8dbbff}.stats .horde strong{color:#ef9995}nav{display:flex;gap:25px;padding:26px 0}a{color:#bcc7d5;text-decoration:none;font-size:14px}a:hover{color:#efd69f}.panel{background:#111923;border:1px solid #293241;border-radius:14px;overflow:hidden;scroll-margin-top:20px}.heading{padding:24px;display:flex;align-items:center;justify-content:space-between;gap:18px;border-bottom:1px solid #27303c}.filters{display:flex;gap:8px}input,select{font:inherit;font-size:13px;background:#0c131c;border:1px solid #354153;color:#dfe5ec;padding:11px;border-radius:7px}input{width:250px}input:focus,select:focus,a:focus-visible{outline:2px solid #c3a56f;outline-offset:3px}.table-wrap{overflow-x:auto}table{width:100%;border-collapse:collapse;text-align:left;white-space:nowrap}th{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:#919eaf;padding:15px 22px}td{font-size:13px;padding:15px 22px;border-top:1px solid #202b38;color:#b5c0ce}td:first-child{color:#eee6d9;font-weight:600}td small{display:block;color:#8b98aa;font-size:11px;margin-top:4px}.tag{font-size:11px;border-radius:4px;padding:4px 7px;background:#263347;color:#a6c8fb}.tag.horde{background:#38282e;color:#eab0a7}.columns{display:grid;grid-template-columns:1.25fr 1fr;gap:22px;margin-top:22px;align-items:start}.feed{max-height:660px;overflow-y:auto;padding:0 24px}.entry{padding:22px 0;border-bottom:1px solid #293241}.entry:last-child{border:0}.entry .meta{font-size:10px;color:#a6b0bd;text-transform:uppercase;letter-spacing:.8px}.entry h3{margin:10px 0;color:#d8c19a}.entry p{font-size:14px;line-height:1.7;white-space:pre-wrap;margin:0;color:#b6c1d0;overflow-wrap:anywhere}.group-body{padding:24px}.group{background:#17212e;border-radius:9px;padding:16px;margin-bottom:12px;font-size:14px;line-height:1.6}.group strong{display:block;color:#dfcfb2}.group p{font-size:12px;color:#aab7c8;margin:5px 0}.guild-title{margin-top:30px}progress{width:100%;height:6px;accent-color:#bea573}.empty{color:#95a2b4;font-size:13px;line-height:1.7;padding:20px;margin:0}footer{margin-top:30px;color:#8593a6;font-size:12px;line-height:1.9}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:850px){.columns{grid-template-columns:1fr}.heading{align-items:flex-start;flex-direction:column}.filters{width:100%}input{width:100%}.filters label:first-child{flex:1}}@media(max-width:540px){main{padding:28px 16px}.stats{grid-template-columns:1fr 1fr;gap:10px}.stats article{padding:17px}.stats strong{font-size:34px}header{display:block}.badge{display:inline-block;margin-top:8px}.heading{padding:20px}.filters{flex-direction:column}select{max-width:100%}nav{gap:20px}nav a{font-size:13px}}