Files
openrig-studio/app/surfaces/floor.html
T
Mike Schwarz 652d2980b5 FLOOR: finished work, and find by id (#1)
A person looking at a rig's floor can now see its finished work, with each item's answer first in plain words, and find an item by id. Coverage is honest: a bounded or unreadable read says so instead of claiming there's nothing. An artifact reference wraps without changing its text.

Six files (+291/-8): the floor page and server, the runtime contract, and tests. Source-only entry; no tag, pin, release or deploy.
2026-10-23 01:45:42 -07:00

367 lines
21 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rig Floor</title>
<style>
:root {
color-scheme: dark;
--bg: #0b0e13;
--panel: #121722;
--line: #252c3a;
--edge2: #2c3444;
--text: #f3f5f8;
--bone: #f3f5f8;
--body: #cbd1db;
--muted: #929cad;
--faint: #929cad;
--working: #47d18c;
--idle: #6d9eff;
--blocked: #ffb44a;
--stalled: #ff795f;
--down: #77808f;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at 80% 0%, #172032 0, transparent 34%), var(--bg);
color: var(--text);
font: 14px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
main { width: min(1040px, 100%); margin: 0 auto; padding: 28px; }
header { display: flex; align-items: end; justify-content: space-between; gap: 18px; margin-bottom: 24px; }
.eyebrow { color: var(--working); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; }
h1 { margin: 4px 0 0; font: 600 clamp(28px, 5vw, 46px)/1.05 system-ui, sans-serif; letter-spacing: -.04em; }
.connection { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 12px; }
.pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--blocked); box-shadow: 0 0 0 4px #ffb44a18; }
.connection.live .pulse { background: var(--working); box-shadow: 0 0 0 4px #47d18c18; }
.connection.polling .pulse { background: var(--idle); box-shadow: 0 0 0 4px #6d9eff18; }
.metrics, .table-wrap, .notice { border: 1px solid var(--line); background: color-mix(in srgb, var(--panel) 94%, transparent); border-radius: 8px; }
.metrics { display: flex; flex-wrap: wrap; align-items: center; margin-bottom: 20px; overflow: hidden; }
.metric { display: flex; align-items: baseline; gap: 8px; padding: 10px 14px; border-right: 1px solid var(--line); }
.metric:last-child { border-right: 0; }
.metric b { font: 650 17px/1 system-ui, sans-serif; }
.metric span, .empty { color: var(--muted); font-size: 11px; }
section { margin-top: 24px; }
h2 { margin: 0 0 10px; color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; }
.welcome { border: 1px solid var(--edge2); border-radius: 3px; padding: 18px 20px; margin-bottom: 22px;
background: rgba(255,255,255,.02); }
.welcome h2 { margin: 0 0 8px; font-size: 15px; letter-spacing: .01em; }
.welcome p { margin: 0 0 12px; color: var(--body); font-size: 12.5px; line-height: 1.65; max-width: 68ch; }
.welcome .wfoot { margin: 12px 0 0; color: var(--faint); font-size: 11.5px; }
.wagents { display: flex; flex-wrap: wrap; gap: 10px; }
.wagent { display: flex; flex-direction: column; gap: 3px; text-align: left; cursor: pointer;
background: rgba(255,255,255,.04); border: 1px solid var(--edge2); border-radius: 3px;
padding: 10px 14px; color: var(--body); font: inherit; min-width: 210px; }
.wagent:hover { border-color: rgba(255,255,255,.4); color: var(--bone); }
.wagent b { font-size: 12.5px; }
.wagent span { font-size: 11px; color: var(--faint); }
.table-wrap { overflow-x: auto; }
table { width: 100%; min-width: 720px; border-collapse: collapse; text-align: left; }
th { padding: 8px 12px; border-bottom: 1px solid var(--line); color: var(--muted); background: rgba(255,255,255,.018); font-size: 9px; font-weight: 650; letter-spacing: .16em; text-transform: uppercase; white-space: nowrap; }
td { padding: 9px 12px; border-bottom: 1px solid var(--line); color: #cbd1db; font-size: 11px; vertical-align: middle; }
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover td { background: rgba(255,255,255,.018); }
.seat-name { color: var(--text); font-weight: 650; overflow-wrap: anywhere; }
.state-tag { --state: var(--down); display: inline-flex; align-items: center; gap: 7px; color: var(--state); font-size: 10px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; white-space: nowrap; }
.state-pip { width: 7px; height: 7px; border-radius: 50%; background: var(--state); box-shadow: 0 0 0 3px color-mix(in srgb, var(--state) 12%, transparent); }
.numeric { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.working { --state: var(--working); }
.idle { --state: var(--idle); }
.blocked { --state: var(--blocked); }
.stalled { --state: var(--stalled); }
.down { --state: var(--down); }
.queue-title { color: var(--text); font-weight: 600; }
.queue-state { color: var(--idle); font-size: 10px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; }
.route { color: var(--muted); overflow-wrap: anywhere; }
.item-id { display: block; margin-top: 3px; color: var(--muted); font: 10px ui-monospace, SFMono-Regular, Menlo, monospace; overflow-wrap: anywhere; }
.result { max-width: 40ch; white-space: pre-wrap; overflow-wrap: anywhere; }
.artifact { color: var(--muted); overflow-wrap: break-word; }
.unset { color: var(--muted); font-style: italic; }
.coverage { margin: -4px 0 10px; color: var(--muted); font-size: 11px; }
/* An element marked hidden stays hidden, whatever display a class gives it (the find box is display:flex). */
[hidden] { display: none !important; }
.find { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin: 0 0 18px; }
.find input { flex: 1 1 260px; max-width: 420px; min-width: 0; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); color: var(--text); font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; }
.find output { color: var(--muted); font-size: 11px; }
.notice { padding: 18px; color: var(--blocked); }
footer { margin-top: 22px; color: var(--muted); font-size: 11px; }
@media (max-width: 600px) {
main { padding: 18px; }
header { align-items: start; flex-direction: column; }
.metric { flex: 1 1 auto; }
}
</style>
</head>
<body>
<main>
<header>
<div><div class="eyebrow">Live activity</div><h1 id="rig">Rig floor</h1></div>
<div id="connection" class="connection"><span class="pulse"></span><span>connecting</span></div>
</header>
<div class="find" id="findbox" role="search" hidden><input id="find" type="search" placeholder="Find work by id (qitem-…)" aria-label="Find work by id" autocomplete="off" spellcheck="false"><output id="found" aria-live="polite"></output></div>
<div id="content"><div class="notice">Reading the runtime contract…</div></div>
<footer id="updated"></footer>
</main>
<script>
const content = document.querySelector('#content');
const rig = document.querySelector('#rig');
const updated = document.querySelector('#updated');
const connection = document.querySelector('#connection');
let events;
let pollTimer;
let reconnectTimer;
const escapeHtml = (value) => String(value ?? '').replace(/[&<>'"]/g, char => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;'
})[char]);
function setConnection(mode, label) {
connection.className = `connection ${mode}`;
connection.lastElementChild.textContent = label;
}
function render(state) {
if (!state.ok) {
content.innerHTML = `<div class="notice">Data degraded: ${escapeHtml(state.degraded || 'unknown reason')}</div>`;
rig.textContent = 'Rig floor';
updated.textContent = '';
return;
}
// NOT ATTACHED is a different fact from ATTACHED-AND-QUIET, and rendering
// them the same way is how a box with no rig showed an empty floor under a
// green live signal — the fiction this surface exists to stop. The
// generator distinguishes three causes; a viewer can only act on the
// difference if the floor says which one it is.
if (state.attached === false) {
const why = {
'no-rig': 'No rig is running on this box.',
'no-rig-cli': 'The rig CLI is not installed on this box.',
'rig-error': 'The rig is installed but could not be read.',
}[state.reason] || 'This box is not attached to a rig.';
rig.textContent = 'Not attached';
content.innerHTML = `<div class="notice"><strong>${escapeHtml(why)}</strong>` +
(state.detail ? `<br/><span>${escapeHtml(state.detail)}</span>` : '') +
`<br/><span>This is not an idle rig — there is nothing attached to report on.</span></div>`;
updated.textContent = state.generatedAt ? `checked ${new Date(state.generatedAt).toLocaleTimeString()}` : '';
setConnection('down', state.reason === 'rig-error' ? 'rig unreadable' : 'no rig');
return;
}
rig.textContent = state.rig;
const working = state.seats.filter(seat => seat.state === 'working').length;
const attention = state.seats.filter(seat => ['blocked', 'stalled', 'down'].includes(seat.state)).length;
const pending = state.queue.length;
const seats = state.seats.map(seat => `
<tr>
<td class="seat-name">${escapeHtml(seat.seat)}</td>
<td>${escapeHtml(seat.pod)} / ${escapeHtml(seat.member)}</td>
<td><span class="state-tag ${escapeHtml(seat.state)}"><span class="state-pip"></span>${escapeHtml(seat.state)}</span></td>
<td>${escapeHtml(seat.lifecycle)}</td>
<td class="numeric">${seat.ageMinutes == null ? 'unknown' : `${escapeHtml(seat.ageMinutes)}m`}</td>
<td class="numeric">${escapeHtml(seat.pendingWork)}</td>
</tr>`).join('') || '<tr><td class="empty" colspan="6">No seats reported.</td></tr>';
// Ids and the find box come with finished work: a generator that sends no `done` gets the floor exactly as before.
const showIds = !!(state.done && Array.isArray(state.done.rows));
document.querySelector('#findbox').hidden = !showIds;
const queue = state.queue.map(item => `
<tr${showIds ? ` data-id="${escapeHtml(item.id)}"` : ''}>
<td class="queue-title">${escapeHtml(item.title)}${showIds ? `<span class="item-id">${escapeHtml(item.id)}</span>` : ''}</td>
<td class="queue-state">${escapeHtml(item.state)}</td>
<td class="route">${escapeHtml(item.source)}</td>
<td class="route">${escapeHtml(item.destination)}</td>
</tr>`).join('') || '<tr><td class="empty" colspan="4">Queue is clear.</td></tr>';
// While a find is on, a table with no matching row says so instead of standing empty under its count.
const queueRows = queue + (showIds && state.queue.length ? '<tr id="nomatch-open" hidden><td class="empty" colspan="4">No match in the queue.</td></tr>' : '');
// Work that finished, from a provider that sends `done` (an older one doesn't, and then this section isn't drawn). The answer
// a seat closed the work with is its result; a field the work doesn't carry reads "not recorded", and a result the provider
// couldn't read says so instead of looking empty.
const unset = (text) => `<span class="unset">${text}</span>`;
// A closure in plain words; the queue's own word stays in the tooltip.
const SAY = { 'no-follow-on': 'Done, nothing follows', handed_off_to: 'Handed off to', blocked_on: 'Blocked on', denied: 'Denied', canceled: 'Canceled', escalation: 'Escalated' };
const decisionOf = (d) => !d?.reason ? unset('not recorded')
: `<span title="${escapeHtml(d.reason)}">${escapeHtml(SAY[d.reason] || d.reason)}${d.target ? `${SAY[d.reason] && /to$|on$/.test(SAY[d.reason]) ? ' ' : ': '}${escapeHtml(d.target)}` : ''}</span>`;
// An artifact reference may break after "@", ":", "/" or "#", not inside a word.
// Split the raw text first and escape each part: inserting after escaping would cut an entity (&#39;) apart.
const breakable = (t) => String(t).split(/(?<=[@:/#])/).map(escapeHtml).join('<wbr>');
const done = state.done && Array.isArray(state.done.rows) ? state.done : null;
const doneRows = done ? done.rows.map(item => {
if (!item || typeof item !== 'object') return '';
const decision = decisionOf(item.decision);
const answer = item.result ?? item.resolution;
const result = item.resultRead === false ? unset("couldn't be read") : typeof answer === 'string' && answer !== '' ? escapeHtml(answer) : unset('not recorded');
const closed = Date.parse(item.closedAt || '');
return `
<tr data-id="${escapeHtml(item.id)}">
<td class="queue-title">${escapeHtml(item.title)}<span class="item-id">${escapeHtml(item.id)}</span></td>
<td class="result">${result}</td>
<td class="route">${decision}</td>
<td class="artifact">${item.artifact ? breakable(item.artifact) : unset('not recorded')}</td>
<td class="numeric">${Number.isFinite(closed) ? escapeHtml(new Date(closed).toLocaleString(undefined, { timeZoneName: 'short' })) : unset('unknown')}</td>
</tr>`; }).join('') || `<tr><td class="empty" colspan="5">None of this rig's work among the newest ${escapeHtml(done.coverage?.looked ?? 0)} finished items on this box.</td></tr>` : '';
const cov = done?.coverage || {};
const doneSection = !done ? '' : cov.unreadable
? `<section><h2>Done recently</h2><div class="notice">Finished work couldn't be read: ${escapeHtml(cov.unreadable)}.</div></section>`
: `<section>
<h2>Done recently · ${done.rows.length}</h2>
<p class="coverage">The newest ${escapeHtml(done.rows.length)} of this rig's finished work, from the newest ${escapeHtml(cov.looked ?? 0)} finished items on this box.</p>
<div class="table-wrap"><table aria-label="Finished work">
<thead><tr><th>Work</th><th>Result</th><th>Decision</th><th>Artifact</th><th class="numeric">Closed</th></tr></thead>
<tbody>${doneRows}${done.rows.length ? '<tr id="nomatch-done" hidden><td class="empty" colspan="5">No match in finished work.</td></tr>' : ''}</tbody>
</table></div>
</section>`;
content.innerHTML = `
${welcomeHtml()}
<div class="metrics" aria-label="Rig summary">
<div class="metric"><b>${working}</b><span>working now</span></div>
<div class="metric"><b>${attention}</b><span>need attention</span></div>
<div class="metric"><b>${pending}</b><span>queue items</span></div>
</div>
<section>
<h2>Seats · ${state.seats.length}</h2>
<div class="table-wrap"><table aria-label="Rig seats">
<thead><tr><th>Seat</th><th>Pod / member</th><th>State</th><th>Lifecycle</th><th class="numeric">Age</th><th class="numeric">Pending</th></tr></thead>
<tbody>${seats}</tbody>
</table></div>
</section>
<section>
<h2>Queue · ${state.queue.length}</h2>
<div class="table-wrap"><table aria-label="Rig queue">
<thead><tr><th>Work</th><th>State</th><th>Source</th><th>Destination</th></tr></thead>
<tbody>${queueRows}</tbody>
</table></div>
</section>${doneSection}`;
lastState = state;
findWork();
updated.textContent = `Runtime snapshot · ${new Date(state.generatedAt).toLocaleString()}`;
}
// Find by id: only among the work this floor already holds (this rig's open work and its newest finished work); it reads
// nothing else. Kept outside #content so a refresh doesn't clear what was typed, and applied again after every refresh.
let lastState = null;
const findInput = document.querySelector('#find'), found = document.querySelector('#found');
// "This rig's" work is work with one of this floor's seats at either end: a generator that still sends other rigs' open work
// can't make the find box call that ours.
function findWork() {
const q = findInput.value.trim();
const seats = new Set((lastState?.seats || []).map(s => s.seat));
const ours = item => item && (seats.has(item.source) || seats.has(item.destination));
const hit = item => ours(item) && String(item.id).includes(q);
const openHits = (lastState?.queue || []).filter(hit), doneHits = (lastState?.done?.rows || []).filter(hit);
const shown = new Set([...openHits, ...doneHits].map(item => String(item.id)));
for (const row of content.querySelectorAll('tr[data-id]')) row.hidden = !!q && !shown.has(row.dataset.id);
for (const [sel, hits] of [['#nomatch-open', openHits], ['#nomatch-done', doneHits]]) { const r = document.querySelector(sel); if (r) r.hidden = !q || hits.length > 0; }
if (!q) { found.textContent = ''; return; }
const done = lastState?.done;
found.textContent = openHits.length || doneHits.length ? `${openHits.length} open, ${doneHits.length} finished`
: done?.coverage?.unreadable ? "Not in this rig's open work. Finished work couldn't be read, so this can't say whether it finished."
: `No work with that id in this rig's open work or its newest ${done?.rows?.length ?? 0} finished items (older finished work isn't searched).`;
}
findInput.addEventListener('input', findWork);
// The box's own introduction, declared in its manifest. Absent on a box
// that declares none, which is the normal case — this surface ships to
// every install and must not assume it is anybody's landing page.
let welcome = null;
function welcomeHtml() {
if (!welcome) return '';
const agents = (welcome.agents || []).filter(a => a && a.seat).map(a => `
<button class="wagent" data-seat="${escapeHtml(a.seat)}">
<b>${escapeHtml(a.label || a.seat)}</b>
<span>${escapeHtml(a.hint || '')}</span>
</button>`).join('');
return `<section class="welcome">
${welcome.title ? `<h2>${escapeHtml(welcome.title)}</h2>` : ''}
${welcome.body ? `<p>${escapeHtml(welcome.body)}</p>` : ''}
${agents ? `<div class="wagents">${agents}</div>` : ''}
${welcome.footer ? `<p class="wfoot">${escapeHtml(welcome.footer)}</p>` : ''}
</section>`;
}
// Summoning a seat is a CONTRACT message (open-agent), not a private
// channel invented here — the shell owns the sidebar and this surface only
// asks. Delegated so it survives the innerHTML rewrite on every refresh.
content.addEventListener('click', (e) => {
const b = e.target.closest('.wagent');
if (b) parent.postMessage({ t: 'open-agent', seat: b.dataset.seat }, '*');
});
async function loadWelcome() {
try {
const r = await fetch('/surfaces.json', { cache: 'no-store' });
const d = await r.json();
welcome = d && typeof d.welcome === 'object' ? d.welcome : null;
} catch { welcome = null; }
}
async function refresh() {
try {
const response = await fetch('/api/factory/state', { cache: 'no-store' });
const state = await response.json();
render(state);
} catch (error) {
content.innerHTML = `<div class="notice">Unable to read floor state: ${escapeHtml(error.message)}</div>`;
}
}
function startPolling() {
clearInterval(pollTimer);
pollTimer = setInterval(refresh, 15000);
setConnection('polling', 'polling fallback');
}
function connectEvents() {
clearTimeout(reconnectTimer);
if (events) events.close();
events = new EventSource('/api/events');
events.onopen = () => {
clearInterval(pollTimer);
setConnection('live', 'live signal');
refresh();
};
events.onmessage = () => refresh();
events.addEventListener('degraded', () => {
events.close();
startPolling();
reconnectTimer = setTimeout(connectEvents, 15000);
});
events.onerror = () => {
events.close();
startPolling();
reconnectTimer = setTimeout(connectEvents, 15000);
};
}
async function boot() {
try {
const response = await fetch('/api/contract', { cache: 'no-store' });
const contract = await response.json();
const needed = ['observe.factory-state', 'stream.events'];
const missing = needed.filter(capability => !contract.capabilities.includes(capability));
// contract-meta compatibility rule: capabilities present + same major.
// (Exact-match rejects contract-compatible minor runtimes.)
const major = String(contract.contractVersion ?? '').split('.')[0];
if (major !== '0' || missing.length) {
throw new Error(`Unsupported runtime contract ${contract.contractVersion}${missing.length ? `; missing ${missing.join(', ')}` : ''}`);
}
await loadWelcome();
await refresh();
connectEvents();
} catch (error) {
content.innerHTML = `<div class="notice">Cannot start: ${escapeHtml(error.message)}</div>`;
startPolling();
}
}
boot();
</script>
</body>
</html>