igneum/tools/workers/page/workers.html
igneum-labs 71881c1926 Worker dashboard: collector on igneum-build-1, Mac pusher, workers.html next to the fleet page (the project lead, 6 October 2026)
tools/workers/collect.mjs runs every 30 s on the box (infra/build-server/workers/igneum-workers.{service,timer},
install.sh) and writes /srv/workers/workers.json from the machine itself: /proc/stat deltas per core, meminfo, df on
/srv, net bytes, hwmon temperatures, the flock state of /srv/builds/_locks, cargo processes with worktree, target and
start time, flock waiters, /srv/builds/_log/builds.jsonl (the format agreed with the build-server agent), sccache
--show-stats, headline.json. tools/workers/push.mjs (launchd every 60 s) adds the Mac's with-lock slots and waiters
and the two PCs' relay job states from the intake, drops them on the box so its file is whole, merges the box's file
and writes the fleet folder's workers.json; it deploys only when the live copy is over 6 min old, otherwise the
fleet orchestrator's 5-minute deploy carries it. The page (tools/workers/page/workers.html, shape.js) tries
https://build.igneum.network/workers.json first and falls back to the folder copy; core strip, arcs, now building,
queue, recently done with closing lines, headline timings, analytics; UK time with UTC tooltips; phone width.
node --test tools/workers/test: 13 tests over /proc/stat, lock dir, JSONL and sccache fixtures and the page shaping.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:59:07 +00:00

366 lines
41 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="robots" content="noindex">
<title>Igneum Workers</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Unbounded:wght@500;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap">
<style>
:root {
--bg: #0c0c0e; --panel: #141418; --panel-2: #1b1b21; --ink: #f1ede4; --muted: #8e8b86; --line: #26262d;
--ember: #f2541b; --ember-2: #ffb15a; --good: #5fd08a; --warn: #f2c14e; --bad: #ff6b5e; --cool: #6aa9ff;
--display: "Unbounded", "IBM Plex Sans", -apple-system, sans-serif;
--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--sans: "IBM Plex Sans", -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
color-scheme: dark;
}
* { box-sizing: border-box; }
body { background: var(--bg); color: var(--ink); font-family: var(--sans); font-size: 14px; line-height: 1.45; margin: 0; }
.wrap { max-width: 1180px; margin: 0 auto; padding-inline: 16px; padding-block: 18px 48px; }
header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 20px; }
.brand { display: flex; align-items: center; gap: 12px; }
.brand svg { width: 34px; height: 34px; }
.brand h1 { font-family: var(--display); font-weight: 700; font-size: 20px; letter-spacing: 0.02em; margin: 0; }
.brand h1 small { font-family: var(--mono); font-weight: 400; font-size: 11px; color: var(--muted); letter-spacing: 0.1em; text-transform: uppercase; margin-left: 10px; }
nav { display: flex; gap: 2px; border: 1px solid var(--line); }
nav a { font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); text-decoration: none; padding: 7px 12px; }
nav a.on { color: var(--ink); background: var(--panel-2); }
nav a:hover { color: var(--ember-2); }
.live { font-family: var(--mono); font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 8px; }
.live b { width: 8px; height: 8px; border-radius: 50%; background: var(--good); box-shadow: 0 0 0 0 rgba(95,208,138,.6); animation: ping 2.4s infinite; }
.live.stale b { background: var(--warn); animation: none; }
.live.down b { background: var(--bad); animation: none; }
@keyframes ping { 0% { box-shadow: 0 0 0 0 rgba(95,208,138,.55); } 70% { box-shadow: 0 0 0 9px rgba(95,208,138,0); } 100% { box-shadow: 0 0 0 0 rgba(95,208,138,0); } }
h2 { font-family: var(--display); font-weight: 500; font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); margin: 32px 0 12px; display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
h2 span { font-family: var(--mono); font-size: 11px; letter-spacing: 0; text-transform: none; color: var(--muted); }
.empty { padding: 18px; color: var(--muted); border: 1px dashed var(--line); font-family: var(--mono); font-size: 12px; line-height: 1.6; }
.pill { display: inline-block; font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; padding: 3px 7px; border: 1px solid var(--line); color: var(--muted); white-space: nowrap; }
.pill.running, .pill.done, .pill.ok { border-color: var(--good); color: var(--good); }
.pill.failed, .pill.timeout, .pill.aborted, .pill.error { border-color: var(--bad); color: var(--bad); }
.pill.waiting, .pill.queued, .pill.stale { border-color: var(--warn); color: var(--warn); }
.pill.kind { border-color: #3a2a22; color: var(--ember-2); }
time[title] { cursor: help; border-bottom: 1px dotted var(--line); }
/* the server */
.server { margin-top: 18px; background: var(--panel); border: 1px solid var(--line); position: relative; overflow: hidden; }
.server.hot::after { content: ""; position: absolute; inset: auto -30% -70% -30%; height: 90%; background: radial-gradient(ellipse at center, rgba(242,84,27,.18), transparent 60%); pointer-events: none; animation: breathe 3.2s ease-in-out infinite; }
@keyframes breathe { 0%, 100% { opacity: .5; } 50% { opacity: 1; } }
.server .head { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: flex-end; gap: 10px 20px; padding: 16px 18px 0; }
.server .name { font-family: var(--display); font-weight: 700; font-size: 22px; letter-spacing: 0.01em; }
.server .name small { font-family: var(--mono); font-weight: 400; font-size: 11px; color: var(--muted); margin-left: 10px; letter-spacing: 0.08em; text-transform: uppercase; }
.server .facts { font-family: var(--mono); font-size: 11px; color: var(--muted); display: flex; flex-wrap: wrap; gap: 4px 16px; }
.server .facts b { color: var(--ink); font-weight: 500; }
.cores { display: grid; grid-template-columns: repeat(48, 1fr); gap: 2px; padding: 14px 18px 0; height: 78px; align-items: end; position: relative; z-index: 1; }
.cores i { display: block; height: 100%; background: var(--panel-2); position: relative; overflow: hidden; }
.cores i::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: var(--h, 2%); min-height: 2px; background: linear-gradient(180deg, var(--ember-2), var(--ember)); transition: height .9s cubic-bezier(.2,.8,.2,1); }
.cores i.hot::after { animation: flicker 1.3s ease-in-out infinite alternate; }
@keyframes flicker { from { filter: brightness(.85); } to { filter: brightness(1.15); } }
.corelabel { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 2px 12px; font-family: var(--mono); font-size: 10px; color: var(--muted); padding: 6px 18px 0; letter-spacing: 0.08em; text-transform: uppercase; }
.gauges { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1px; background: var(--line); border-top: 1px solid var(--line); margin-top: 14px; position: relative; z-index: 1; }
@media (min-width: 900px) { .gauges { grid-template-columns: repeat(5, 1fr); } }
.g { background: var(--panel); padding: 12px 14px; display: flex; gap: 12px; align-items: center; }
.g svg { width: 56px; height: 56px; flex: none; }
.g .ring { fill: none; stroke: var(--panel-2); stroke-width: 6; }
.g .arc { fill: none; stroke: var(--ember); stroke-width: 6; stroke-linecap: round; transform: rotate(-90deg); transform-origin: 50% 50%; transition: stroke-dasharray .9s ease; }
.g .arc.good { stroke: var(--good); } .g .arc.warn { stroke: var(--warn); } .g .arc.bad { stroke: var(--bad); } .g .arc.cool { stroke: var(--cool); }
.g .l { font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
.g .v { font-family: var(--display); font-weight: 500; font-size: 18px; margin-top: 2px; font-variant-numeric: tabular-nums; line-height: 1.1; }
.g .s { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 3px; }
/* the other workers */
.crew { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
.w { background: var(--panel); border: 1px solid var(--line); padding: 12px 14px; }
.w .top { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
.w .card { font-family: var(--display); font-weight: 500; font-size: 13px; }
.w .meta { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 3px; }
.w .doing { font-size: 12px; margin-top: 8px; min-height: 2.6em; }
.w .foot { display: flex; justify-content: space-between; font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 8px; border-top: 1px solid var(--line); padding-top: 8px; }
.slots { display: flex; gap: 4px; margin-top: 8px; }
.slots i { height: 6px; flex: 1; background: var(--panel-2); }
.slots i.held { background: linear-gradient(90deg, var(--ember), var(--ember-2)); }
.slots i.run { background: var(--cool); }
.slots i.measure { background: var(--warn); }
/* lanes */
.lane { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
.job { background: var(--panel); border: 1px solid var(--line); padding: 12px 14px; position: relative; overflow: hidden; }
.job.running { border-color: #3a2a22; box-shadow: inset 0 0 0 1px rgba(242,84,27,.25); }
.job.running::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: linear-gradient(180deg, var(--ember-2), var(--ember)); }
.job .top { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
.job .title { font-family: var(--display); font-weight: 500; font-size: 13px; line-height: 1.3; word-break: break-word; }
.job .where { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 3px; }
.job .cmd { font-family: var(--mono); font-size: 11px; color: var(--ink); margin-top: 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.job .foot { display: flex; justify-content: space-between; align-items: baseline; font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 10px; border-top: 1px solid var(--line); padding-top: 8px; gap: 8px; }
.job .elapsed { font-family: var(--display); font-weight: 500; font-size: 18px; color: var(--ember-2); font-variant-numeric: tabular-nums; }
.job .pulse { position: absolute; right: -40px; bottom: -40px; width: 120px; height: 120px; border-radius: 50%; background: radial-gradient(circle, rgba(242,84,27,.22), transparent 65%); animation: breathe 2.4s ease-in-out infinite; pointer-events: none; }
/* timeline */
.tl { background: var(--panel); border: 1px solid var(--line); }
.tl .row { display: grid; grid-template-columns: 64px 1fr 150px 92px; gap: 10px; align-items: center; padding: 9px 12px; border-bottom: 1px solid var(--line); cursor: pointer; }
.tl .row:hover { background: var(--panel-2); }
.tl .row:last-child, .tl .row.open { border-bottom-color: transparent; }
.tl .at { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.tl .what { min-width: 0; }
.tl .what .t { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tl .what .m { font-family: var(--mono); font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tl .bar { height: 8px; background: var(--panel-2); position: relative; }
.tl .bar i { position: absolute; left: 0; top: 0; bottom: 0; background: linear-gradient(90deg, var(--good), #8ee3ad); }
.tl .bar i.failed { background: linear-gradient(90deg, var(--bad), #ff9b92); }
.tl .dur { font-family: var(--mono); font-size: 12px; text-align: right; font-variant-numeric: tabular-nums; }
.tl .lines { display: none; font-family: var(--mono); font-size: 11px; color: var(--muted); padding: 2px 12px 12px 86px; border-bottom: 1px solid var(--line); white-space: pre-wrap; word-break: break-all; line-height: 1.6; }
.tl .lines.show { display: block; }
.tl .lines b { color: var(--ink); font-weight: 500; }
@media (max-width: 640px) { .tl .row { grid-template-columns: 52px 1fr 70px; } .tl .bar { display: none; } .tl .lines { padding-left: 12px; } }
/* analytics */
.an { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; }
.an .a { background: var(--panel); border: 1px solid var(--line); padding: 12px 14px; min-width: 0; }
.an .l { font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); display: flex; justify-content: space-between; gap: 8px; }
.an .l span { letter-spacing: 0; text-transform: none; font-family: var(--mono); }
.an svg { width: 100%; height: 96px; display: block; margin-top: 10px; }
.an table { border-collapse: collapse; width: 100%; font-size: 12px; margin-top: 8px; }
.an th, .an td { text-align: left; padding: 6px 6px; border-bottom: 1px solid var(--line); white-space: nowrap; }
.an th { font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted); font-weight: 600; }
.an td.num, .an th.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
.an tr:last-child td { border-bottom: 0; }
.an .note { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 8px; line-height: 1.5; }
.head3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1px; background: var(--line); border: 1px solid var(--line); }
.h3 { background: var(--panel); padding: 14px 16px; }
.h3 .l { font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
.h3 .v { font-family: var(--display); font-weight: 500; font-size: 26px; margin-top: 6px; font-variant-numeric: tabular-nums; color: var(--ember-2); }
.h3 .v small { font-size: 12px; color: var(--muted); font-family: var(--mono); font-weight: 400; margin-left: 8px; }
.h3 .cmp { display: flex; gap: 14px; font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 6px; flex-wrap: wrap; }
.h3 .cmp b { color: var(--ink); font-weight: 500; }
.h3 .s { font-size: 11px; color: var(--muted); margin-top: 8px; line-height: 1.5; }
.h3 .meter { height: 4px; background: var(--panel-2); margin-top: 10px; position: relative; }
.h3 .meter i { display: block; height: 100%; background: linear-gradient(90deg, var(--ember), var(--ember-2)); transition: width .6s ease; }
.h3 .meter em { position: absolute; top: -3px; width: 2px; height: 10px; background: var(--muted); }
.sources { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 32px; border-top: 1px solid var(--line); padding-top: 10px; display: flex; flex-wrap: wrap; gap: 6px 18px; }
.sources b { color: var(--good); font-weight: 500; } .sources b.no { color: var(--bad); } .sources b.warn { color: var(--warn); }
a:focus-visible, button:focus-visible { outline: 2px solid var(--ember-2); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .live b, .server.hot::after, .job .pulse, .cores i.hot::after { animation: none !important; } .cores i::after, .g .arc { transition: none; } }
</style></head><body>
<div class="wrap">
<header>
<div class="brand">
<svg viewBox="0 0 100 100" aria-hidden="true"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"/><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"/></svg>
<h1>Igneum Workers<small>build shop</small></h1>
</div>
<nav aria-label="Pages"><a href="./">Fleet</a><a href="workers.html" class="on" aria-current="page">Workers</a></nav>
<div class="live" id="live"><b></b><span id="stamp">waiting for the first write</span></div>
</header>
<section class="server" id="server" aria-label="The build server">
<div class="head">
<div><div class="name" id="s-name">igneum-build-1<small>build server</small></div><div class="facts" id="s-facts"></div></div>
<div class="facts" id="s-right"></div>
</div>
<div class="cores" id="cores" aria-label="96 cores, busy share per core"></div>
<div class="corelabel"><span id="cores-l">cores</span><span id="cores-r"></span></div>
<div class="gauges" id="gauges"></div>
</section>
<h2>Workers <span>every machine that builds, tests or measures</span></h2>
<div class="crew" id="crew"></div>
<h2>Now building <span id="now-sub"></span></h2>
<div class="lane" id="now"></div>
<h2>Queue <span id="queue-sub"></span></h2>
<div class="lane" id="queue"></div>
<h2>Recently done <span>click a row for the closing lines</span></h2>
<div class="tl" id="done"></div>
<h2>Headline timings <span>what the box does against the Mac and PC 1 on the same tree</span></h2>
<div class="head3" id="headline"></div>
<h2>Analytics <span id="an-sub"></span></h2>
<div class="an" id="an"></div>
<div class="sources" id="sources"></div>
</div>
<script type="module">
import { durationByKind, buildsPerHour, hitRateSeries, compareKinds, timeline, kindLabel, fmtDur, fmtDurShort, fmtBytes, fmtBps, fmtUptime, ukTime, ago, headline } from './shape.js?v=1';
const $ = id => document.getElementById(id);
const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const pill = (s, cls) => `<span class="pill ${esc(cls || String(s || '').toLowerCase())}">${esc(s || '?')}</span>`;
const t = (isoStr, withDate) => { const u = ukTime(isoStr, { withDate }); return u.text ? `<time title="${esc(u.utc)}" datetime="${esc(isoStr)}">${esc(u.text)}</time>` : ''; };
const base = location.pathname.replace(/\/workers\.html$/, '').replace(/\/$/, '');
const demo = new URLSearchParams(location.search).get('demo');
let D = null, open = new Set();
function arc(pct, cls) {
const r = 24, c = 2 * Math.PI * r, p = Math.max(0, Math.min(100, Number(pct) || 0));
return `<svg viewBox="0 0 60 60" aria-hidden="true"><circle class="ring" cx="30" cy="30" r="${r}"/><circle class="arc ${cls || ''}" cx="30" cy="30" r="${r}" stroke-dasharray="${(c * p / 100).toFixed(1)} ${c.toFixed(1)}"/></svg>`;
}
const tone = (pct, warn = 70, bad = 90) => pct >= bad ? 'bad' : pct >= warn ? 'warn' : '';
function renderServer(b, src) {
const el = $('server');
if (!b) {
$('s-facts').innerHTML = `<span>${esc(src && src.error || 'no box facts yet')}</span>`; $('s-right').innerHTML = '';
$('cores').innerHTML = Array.from({ length: 96 }, () => '<i style="--h:2%"></i>').join(''); $('cores-r').textContent = 'no sample'; $('gauges').innerHTML = ''; el.classList.remove('hot'); return;
}
$('s-name').innerHTML = `${esc(b.name)}<small>${esc(b.os || 'build server')}</small>`;
$('s-facts').innerHTML = [`<b>${b.cores}</b> threads`, `<b>${esc(fmtUptime(b.uptime_s))}</b> up`, `load <b>${(b.load || []).map(x => Number(x).toFixed(1)).join(' / ')}</b>`, b.kernel ? `kernel <b>${esc(b.kernel)}</b>` : ''].filter(Boolean).map(x => `<span>${x}</span>`).join('');
const temps = (b.temps || []).map(x => `${esc(x.name)} <b>${x.c.toFixed(0)}°</b>`);
$('s-right').innerHTML = [...temps, b.net ? `net ↓<b>${esc(fmtBps(b.net.rx_bps))}</b> ↑<b>${esc(fmtBps(b.net.tx_bps))}</b>` : '', `<span title="${esc(b.collected_at)}">read ${esc(ago(b.collected_at))}</span>`].filter(Boolean).map(x => `<span>${x}</span>`).join('');
const per = (b.cpu && b.cpu.per_core) || [];
const n = Math.max(per.length, b.cores || 96);
$('cores').style.gridTemplateColumns = `repeat(${n > 64 ? 48 : n > 32 ? 32 : n}, 1fr)`;
$('cores').innerHTML = Array.from({ length: n }, (_, i) => { const p = per[i] ?? 0; return `<i class="${p >= 50 ? 'hot' : ''}" style="--h:${Math.max(2, p)}%" title="core ${i}: ${p}%"></i>`; }).join('');
$('cores-l').textContent = `${n} cores, ${b.cpu ? b.cpu.cores_busy : 0} busy`;
$('cores-r').textContent = b.cpu ? `${b.cpu.busy_pct}% of the box over the last second, ${b.compilers || 0} compilers running` : '';
el.classList.toggle('hot', !!(b.running && b.running.length));
const m = b.mem || {}, d = b.disk || {}, sc = b.sccache;
const hit = sc && sc.hit_rate_pct !== null && sc.hit_rate_pct !== undefined ? sc.hit_rate_pct : null;
$('gauges').innerHTML = [
`<div class="g">${arc(b.cpu ? b.cpu.busy_pct : 0, tone(b.cpu ? b.cpu.busy_pct : 0, 101, 101))}<div><div class="l">CPU</div><div class="v">${b.cpu ? b.cpu.busy_pct : 0}%</div><div class="s">load 1 min ${b.load ? Number(b.load[0]).toFixed(1) : '?'} of ${b.cores}</div></div></div>`,
`<div class="g">${arc(m.used_pct, tone(m.used_pct))}<div><div class="l">Memory</div><div class="v">${m.used_pct ?? '?'}%</div><div class="s">${esc(fmtBytes((m.used_kb || 0) * 1024))} of ${esc(fmtBytes((m.total_kb || 0) * 1024))}</div></div></div>`,
`<div class="g">${arc(d.used_pct, tone(d.used_pct, 80, 92))}<div><div class="l">Disk ${esc(d.mount || '/srv')}</div><div class="v">${d.used_pct ?? '?'}%</div><div class="s">${esc(fmtBytes(d.used_bytes))} used, ${esc(fmtBytes(d.avail_bytes))} free</div></div></div>`,
`<div class="g">${arc(hit ?? 0, 'cool')}<div><div class="l">sccache hits</div><div class="v">${hit === null ? '–' : hit + '%'}</div><div class="s">${sc ? `${sc.hits ?? 0} hit, ${sc.misses ?? 0} miss, ${esc(sc.cache_size || '?')} of ${esc(sc.max_size || '?')}` : 'sccache not answering'}</div></div></div>`,
`<div class="g">${arc(b.slots ? (b.slots.held.length / Math.max(1, b.slots.count)) * 100 : 0, 'good')}<div><div class="l">Build slots</div><div class="v">${b.slots ? `${b.slots.held.length}/${b.slots.count}` : '–'}</div><div class="s">${b.queue && b.queue.length ? `${b.queue.length} waiting` : 'nobody waiting'}</div></div></div>`,
].join('');
}
function slotBar(slots, count) {
const held = new Map((slots && slots.held || []).map(h => [h.slot, h]));
const names = Array.from({ length: count }, (_, i) => `build-${i}`);
return `<div class="slots">${names.map(nm => `<i class="${held.has(nm) ? 'held' : ''}" title="${esc(nm)}${held.has(nm) ? ': ' + esc(held.get(nm).label) : ': free'}"></i>`).join('')}</div>`;
}
function renderCrew() {
const b = D.box, mac = D.mac, pcs = (D.pcs && D.pcs.machines) || [];
const cards = [];
if (b) cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(b.name)}</div><div class="meta">Hetzner, ${b.cores} threads, ${esc(fmtBytes((b.mem && b.mem.total_kb || 0) * 1024))}, RAID 1 NVMe</div></div>${pill(b.running && b.running.length ? 'building' : 'idle', b.running && b.running.length ? 'running' : '')}</div><div class="doing">${b.running && b.running.length ? b.running.map(r => esc(`${r.worktree || '?'}: ${kindLabel(r.kind)}`)).join('<br>') : 'Slot free. The next build-remote.sh or cross-remote.sh from any worktree takes it.'}</div>${slotBar(b.slots, b.slots ? b.slots.count : 1)}<div class="foot"><span>${b.recent ? b.recent.length : 0} builds logged</span><span>${esc(ago(b.collected_at))}</span></div></div>`);
else cards.push(`<div class="w"><div class="top"><div><div class="card">igneum-build-1</div><div class="meta">build server</div></div>${pill('unreachable', 'error')}</div><div class="doing">${esc(D.sources && D.sources.box && D.sources.box.error || 'no facts from the box')}</div></div>`);
if (mac) {
const held = mac.slots.held || [], runs = held.filter(h => /^run-/.test(h.slot)), builds = held.filter(h => /^build-/.test(h.slot)), meas = held.filter(h => h.slot === 'measure');
const doing = held.length ? held.map(h => `${esc(h.slot)}: ${esc((h.worktree ? h.worktree + ' ' : '') + (h.command || h.label || ''))}`.slice(0, 140)).join('<br>') : 'Nothing under with-lock.sh. macOS binaries and the DMG build here; everything else goes to the box or the PCs.';
cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(mac.name)}</div><div class="meta">this Mac, ${mac.build_slots} build slot${mac.build_slots === 1 ? '' : 's'} open, 3 run slots</div></div>${pill(meas.length ? 'measuring' : builds.length ? 'building' : runs.length ? 'running' : 'idle', held.length ? 'running' : '')}</div><div class="doing">${doing}</div><div class="slots">${['build-0', 'build-1', 'build-2', 'run-0', 'run-1', 'run-2', 'measure'].map(nm => { const h = held.find(x => x.slot === nm); return `<i class="${h ? (nm === 'measure' ? 'measure' : nm.startsWith('run') ? 'run' : 'held') : ''}" title="${esc(nm)}${h ? ': ' + esc(h.label) : ': free'}"></i>`; }).join('')}</div><div class="foot"><span>${mac.queue.length ? mac.queue.length + ' waiting for a slot' : 'nobody waiting'}</span><span>${esc(ago(mac.collected_at))}</span></div></div>`);
} else cards.push(`<div class="w"><div class="top"><div><div class="card">MacBook-Pro</div><div class="meta">this Mac</div></div>${pill('no push', 'error')}</div><div class="doing">${esc(D.sources && D.sources.mac && D.sources.mac.error || 'the Mac pusher has not written yet (launchd com.igneum.workers-push, every 60 s)')}</div></div>`);
for (const pc of pcs) {
const r = pc.running;
cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(pc.name)}</div><div class="meta">${esc(pc.machine || pc.id)} · ${esc(pc.role)}</div></div>${pill(r ? 'running' : pc.queue.length ? 'queued' : 'idle', r ? 'running' : pc.queue.length ? 'queued' : '')}</div><div class="doing">${r ? `${esc(r.kind)} ${esc(r.job)}${r.title ? '<br>' + esc(r.title) : ''}${r.stage ? `<br><span class="pill">stage ${esc(r.stage)}</span>` : ''}` : pc.queue.length ? `Next: ${esc(pc.queue[0].kind)} ${esc(pc.queue[0].job)}${pc.queue[0].title ? ', ' + esc(pc.queue[0].title) : ''}` : esc(pc.note || 'idle on jobs; the relay polls every 10 min')}</div><div class="foot"><span>${pc.recent.length} reports kept</span><span>${pc.last_report_at ? 'last report ' + esc(ago(pc.last_report_at)) : 'no report in 7 days'}</span></div></div>`);
}
if (!pcs.length) cards.push(`<div class="w"><div class="top"><div><div class="card">PC 1 and PC 2</div><div class="meta">relay jobs</div></div>${pill('no data', 'error')}</div><div class="doing">${esc(D.sources && D.sources.pcs && D.sources.pcs.error || 'the Mac pusher reads the relay intake; nothing has arrived')}</div></div>`);
$('crew').innerHTML = cards.join('');
}
function nowItems() {
const out = [];
for (const r of (D.box && D.box.running) || []) out.push({ where: D.box.name, slot: r.slot, title: `${kindLabel(r.kind)}`, sub: `${r.worktree || '?'}/${r.crate || ''}${r.agent ? ' · ' + r.agent : ''}`, cmd: r.command, started_at: r.started_at, kind: r.kind, extra: r.target || '' });
for (const h of (D.mac && D.mac.slots.held) || []) out.push({ where: D.mac.name, slot: h.slot, title: h.slot === 'measure' ? 'measurement' : h.slot.startsWith('run') ? 'functional run' : kindLabel(h.kind), sub: h.worktree || 'this Mac', cmd: h.command || h.label, started_at: h.since, kind: h.kind, extra: h.waited_s ? `waited ${fmtDur(h.waited_s)}` : '' });
for (const pc of (D.pcs && D.pcs.machines) || []) if (pc.running) out.push({ where: pc.name, slot: 'relay job', title: `${pc.running.kind} ${pc.running.job}`, sub: pc.running.title || pc.machine || '', cmd: pc.running.stage ? `stage ${pc.running.stage}` : 'reports every 5 min while it runs', started_at: pc.running.started_at, kind: pc.running.kind, extra: pc.running.last_report_at ? `last report ${ago(pc.running.last_report_at)}` : '' });
return out;
}
function renderNow() {
const items = nowItems();
$('now-sub').textContent = items.length ? `${items.length} in flight` : '';
if (!items.length) { $('now').innerHTML = `<div class="empty">Nothing is building right now. The box's slot is free, the Mac holds no lock and neither PC reports a running job. A build appears here within 30 s of its slot being taken (the box) or 60 s (the Mac and the PCs).</div>`; return; }
$('now').innerHTML = items.map(i => `<div class="job running" data-start="${esc(i.started_at || '')}"><div class="pulse"></div><div class="top"><div><div class="title">${esc(i.title)}</div><div class="where">${esc(i.where)} · ${esc(i.slot || '')}${i.sub ? ' · ' + esc(i.sub) : ''}</div></div>${pill(i.kind ? kindLabel(i.kind) : 'job', 'kind')}</div><div class="cmd" title="${esc(i.cmd)}">${esc(i.cmd || '')}</div><div class="foot"><span>since ${t(i.started_at)}${i.extra ? ' · ' + esc(i.extra) : ''}</span><span class="elapsed">${i.started_at ? fmtDurShort((Date.now() - Date.parse(i.started_at)) / 1000) : '–'}</span></div></div>`).join('');
}
function tick() { for (const el of document.querySelectorAll('.job.running[data-start]')) { const s = el.dataset.start; if (!s) continue; const e = el.querySelector('.elapsed'); if (e) e.textContent = fmtDurShort((Date.now() - Date.parse(s)) / 1000); } }
function renderQueue() {
const items = [];
for (const w of (D.box && D.box.queue) || []) items.push({ where: D.box.name, title: w.label ? kindLabel(w.kind) : 'a build', sub: w.label || 'label unknown: the waiter writes none until its slot is taken', since: w.since, wait: w.waiting_s });
for (const w of (D.mac && D.mac.queue) || []) items.push({ where: D.mac.name, title: `${w.mode} slot`, sub: w.command, since: null, wait: w.waiting_s });
for (const pc of (D.pcs && D.pcs.machines) || []) for (const q of pc.queue) items.push({ where: pc.name, title: `${q.kind} ${q.job}`, sub: q.title || '', since: null, wait: null, until: q.expires_at });
$('queue-sub').textContent = items.length ? `${items.length} waiting` : '';
if (!items.length) { $('queue').innerHTML = `<div class="empty">The queue is empty. On the box a second build waits on the flock of build-0 and shows here; on the Mac a with-lock.sh wait; on a PC a signed job it has not reported on yet.</div>`; return; }
$('queue').innerHTML = items.map(i => `<div class="job"><div class="top"><div><div class="title">${esc(i.title)}</div><div class="where">${esc(i.where)}</div></div>${pill('waiting')}</div><div class="cmd" title="${esc(i.sub)}">${esc(i.sub)}</div><div class="foot"><span>${i.since ? 'since ' + t(i.since) : i.until ? 'signed, expires ' + t(i.until, true) : ''}</span><span>${i.wait !== null && i.wait !== undefined ? 'waiting ' + esc(fmtDur(i.wait)) : ''}</span></div></div>`).join('');
}
function doneRows() {
const rows = [];
for (const b of (D.box && D.box.recent) || []) rows.push({ key: b.id, where: D.box.name, at: b.end || b.start, title: `${kindLabel(b.kind)} · ${b.worktree || '?'}${b.agent ? ' · ' + b.agent : ''}`, meta: b.command || '', ok: b.ok, secs: b.secs, status: b.ok ? 'ok' : `exit ${b.exit}`, lines: b.closing || null, raw: b });
for (const pc of (D.pcs && D.pcs.machines) || []) for (const r of pc.recent) rows.push({ key: `${pc.id}-${r.job}`, where: pc.name, at: r.finished_at, title: `${r.kind} ${r.job}`, meta: r.title || r.summary || '', ok: r.ok, secs: r.secs, status: r.status, lines: [r.summary, ...(r.errors || [])].filter(Boolean), raw: r });
rows.sort((a, b) => String(b.at || '').localeCompare(String(a.at || '')));
return rows;
}
function renderDone() {
const rows = doneRows().slice(0, 40);
const boxRows = timeline((D.box && D.box.recent) || [], { limit: 200 }); const closing = new Map(boxRows.map(r => [r.id, r.closing]));
if (!rows.length) { $('done').innerHTML = `<div class="empty">No finished work yet. The box appends one line per build to /srv/builds/_log/builds.jsonl when bs_remote_run ends${D.box && D.box.log && !D.box.log.present ? ' (the file does not exist yet)' : ''}; the PCs' closing reports come from the relay intake.</div>`; return; }
const max = Math.max(60, ...rows.map(r => Number(r.secs) || 0));
$('done').innerHTML = rows.map(r => {
const w = Math.max(1.5, ((Number(r.secs) || 0) / max) * 100).toFixed(1);
const lines = r.lines || closing.get(r.key) || [];
const isOpen = open.has(r.key);
return `<div class="row ${isOpen ? 'open' : ''}" data-key="${esc(r.key)}" role="button" tabindex="0"><div class="at">${t(r.at)}<br><span style="opacity:.7">${esc(r.where)}</span></div><div class="what"><div class="t">${esc(r.title)}</div><div class="m" title="${esc(r.meta)}">${esc(r.meta)}</div></div><div class="bar"><i class="${r.ok ? '' : 'failed'}" style="width:${w}%"></i></div><div class="dur" style="color:${r.ok ? 'var(--good)' : 'var(--bad)'}">${esc(fmtDurShort(r.secs))}<br><span style="color:var(--muted);font-size:10px">${esc(r.status)}</span></div></div><div class="lines ${isOpen ? 'show' : ''}">${lines.length ? lines.map(l => esc(l)).join('\n') : 'no closing lines kept for this entry'}</div>`;
}).join('');
for (const el of $('done').querySelectorAll('.row')) { const f = () => { const k = el.dataset.key; open.has(k) ? open.delete(k) : open.add(k); renderDone(); }; el.onclick = f; el.onkeydown = e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); f(); } }; }
}
function renderHeadline() {
const rows = headline(D.headline);
$('headline').innerHTML = rows.map(r => {
const max = Math.max(r.box_s || 0, r.mac_s || 0, r.pc_s || 0, 1);
return `<div class="h3"><div class="l">${esc(r.label)}</div><div class="v">${r.box_s === null ? '<span style="color:var(--muted);font-size:14px">not measured yet</span>' : esc(fmtDur(r.box_s)) + '<small>box</small>'}</div><div class="cmp">${r.mac_s !== null ? `Mac <b>${esc(fmtDur(r.mac_s))}</b>${r.speedup ? ` (${r.speedup}× slower)` : ''}` : 'Mac <b>not measured on this tree</b>'}${r.pc_s !== null ? ` · PC 1 <b>${esc(fmtDur(r.pc_s))}</b>` : ''}</div>${r.box_s !== null ? `<div class="meter"><i style="width:${(r.box_s / max * 100).toFixed(1)}%"></i>${r.mac_s !== null ? `<em style="left:${(r.mac_s / max * 100).toFixed(1)}%" title="Mac"></em>` : ''}${r.pc_s !== null ? `<em style="left:${(r.pc_s / max * 100).toFixed(1)}%;background:var(--cool)" title="PC 1"></em>` : ''}</div>` : ''}<div class="s">${esc(r.note || r.what)}${r.measured_at ? ` · measured ${t(r.measured_at, true)}` : ''}</div></div>`;
}).join('');
}
function spark(points, { max = 100, label = v => v } = {}) {
if (!points.length) return '';
const W = 300, H = 90, L = 30, R = 6, T = 8, B = 20;
const X = i => L + (points.length === 1 ? (W - L - R) / 2 : (i / (points.length - 1)) * (W - L - R));
const Y = v => T + (1 - Math.max(0, Math.min(max, v)) / max) * (H - T - B);
const path = points.map((p, i) => `${i ? 'L' : 'M'}${X(i).toFixed(1)} ${Y(p.v).toFixed(1)}`).join(' ');
const area = `${path} L${X(points.length - 1).toFixed(1)} ${(H - B).toFixed(1)} L${X(0).toFixed(1)} ${(H - B).toFixed(1)} Z`;
return `<svg viewBox="0 0 ${W} ${H}" aria-label="${esc(label(max))} at the top"><line x1="${L}" y1="${H - B}" x2="${W - R}" y2="${H - B}" stroke="#26262d"/><line x1="${L}" y1="${T}" x2="${L}" y2="${H - B}" stroke="#26262d"/><path d="${area}" fill="rgba(106,169,255,.16)"/><path d="${path}" fill="none" stroke="#6aa9ff" stroke-width="2"/>${points.map((p, i) => `<circle cx="${X(i).toFixed(1)}" cy="${Y(p.v).toFixed(1)}" r="2.5" fill="#6aa9ff"><title>${esc(p.t)}</title></circle>`).join('')}<text x="4" y="${T + 4}" font-family="IBM Plex Mono, monospace" font-size="9" fill="#8e8b86">${esc(label(max))}</text><text x="4" y="${H - B}" font-family="IBM Plex Mono, monospace" font-size="9" fill="#8e8b86">0</text><text x="${L}" y="${H - 6}" font-family="IBM Plex Mono, monospace" font-size="9" fill="#8e8b86">${esc(points[0].x)}</text><text x="${W - R}" y="${H - 6}" text-anchor="end" font-family="IBM Plex Mono, monospace" font-size="9" fill="#8e8b86">${esc(points[points.length - 1].x)}</text></svg>`;
}
function bars(buckets) {
const W = 300, H = 90, T = 8, B = 18, max = Math.max(1, ...buckets.map(b => b.n));
const bw = (W - 8) / 24;
return `<svg viewBox="0 0 ${W} ${H}" aria-label="builds per hour today"><line x1="4" y1="${H - B}" x2="${W - 4}" y2="${H - B}" stroke="#26262d"/>${buckets.map((b, i) => { const h = (b.n / max) * (H - T - B); const ok = b.n ? (b.ok / b.n) * h : 0; const x = 4 + i * bw; return `<g><title>${String(i).padStart(2, '0')}:00 UTC: ${b.n} build${b.n === 1 ? '' : 's'}, ${b.failed} failed</title><rect x="${(x + 1).toFixed(1)}" y="${(H - B - h).toFixed(1)}" width="${(bw - 2).toFixed(1)}" height="${h.toFixed(1)}" fill="#ff6b5e"/><rect x="${(x + 1).toFixed(1)}" y="${(H - B - ok).toFixed(1)}" width="${(bw - 2).toFixed(1)}" height="${ok.toFixed(1)}" fill="#5fd08a"/></g>`; }).join('')}${[0, 6, 12, 18, 23].map(h => `<text x="${(4 + h * bw + bw / 2).toFixed(1)}" y="${H - 5}" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="9" fill="#8e8b86">${String(h).padStart(2, '0')}Z</text>`).join('')}<text x="4" y="${T + 4}" font-family="IBM Plex Mono, monospace" font-size="9" fill="#8e8b86">${max}</text></svg>`;
}
function renderAnalytics() {
const builds = (D.box && D.box.recent) || [];
const day = new Date().toISOString().slice(0, 10);
const todays = builds.filter(b => String(b.end || b.start || '').startsWith(day));
$('an-sub').textContent = builds.length ? `${builds.length} logged builds, ${todays.length} today, ${todays.filter(b => b.ok).length} passed` : '';
if (!builds.length) { $('an').innerHTML = `<div class="empty">Analytics start with the first logged build on the box. The log is /srv/builds/_log/builds.jsonl${D.box && D.box.log && D.box.log.present ? ` (${D.box.log.total} lines, ${D.box.log.bad_lines} unreadable)` : ' (not written yet)'}.</div>`; return; }
const ph = buildsPerHour(builds, day);
const dk = durationByKind(builds);
const hr = hitRateSeries(builds);
const cmp = compareKinds(builds, D.baselines || {});
const ukNow = ukTime(new Date().toISOString());
$('an').innerHTML = [
`<div class="a"><div class="l">Builds per hour today<span>${todays.length} today, hours in UTC</span></div>${bars(ph)}<div class="note">Green passed, red failed. Local time now ${esc(ukNow.text)} (${esc(ukNow.utc.slice(11))}).</div></div>`,
`<div class="a"><div class="l">Duration by kind<span>median and p90, all logged</span></div><table><thead><tr><th>Kind</th><th class="num">n</th><th class="num">median</th><th class="num">p90</th><th class="num">max</th></tr></thead><tbody>${dk.map(d => `<tr><td>${esc(d.label)}</td><td class="num">${d.n}</td><td class="num">${esc(fmtDurShort(d.median_s))}</td><td class="num">${esc(fmtDurShort(d.p90_s))}</td><td class="num">${esc(fmtDurShort(d.max_s))}</td></tr>`).join('')}</tbody></table></div>`,
`<div class="a"><div class="l">sccache hit rate over time<span>per build</span></div>${hr.length ? spark(hr.map(p => ({ v: p.pct, t: `${ukTime(p.at).utc}: ${p.pct}% (${kindLabel(p.kind)})`, x: ukTime(p.at).text })), { max: 100, label: v => v + '%' }) : '<div class="empty">no sccache figures in the log yet</div>'}<div class="note">A cold tree misses everything; the second build of the same tree should climb towards 100%.</div></div>`,
`<div class="a"><div class="l">Box against Mac<span>same kind, median</span></div>${cmp.length ? `<table><thead><tr><th>Kind</th><th class="num">box</th><th class="num">Mac</th><th class="num">×</th></tr></thead><tbody>${cmp.map(c => `<tr><td>${esc(c.label)}</td><td class="num">${c.box_s === null ? '–' : esc(fmtDurShort(c.box_s)) + ` <span style="color:var(--muted)">(${c.box_n})</span>`}</td><td class="num" title="${esc(c.mac_source || '')}">${c.mac_s === null ? '–' : esc(fmtDurShort(c.mac_s))}</td><td class="num">${c.speedup === null ? '' : c.speedup + '×'}</td></tr>`).join('')}</tbody></table><div class="note">Mac figures marked approximate come from the docs that measured them, not from this tree; a Mac row goes exact when the same tree is built here under with-lock.sh and logged.</div>` : '<div class="empty">no kinds to compare yet</div>'}</div>`,
].join('');
}
function renderSources() {
const s = D.sources || {};
const one = (name, v, what) => `<span>${esc(name)} <b class="${v && v.ok ? '' : v && v.at ? 'warn' : 'no'}">${v && v.ok ? 'live' : v && v.at ? 'stale' : 'missing'}</b>${v && v.at ? ' ' + esc(ago(v.at)) : ''}${v && v.error ? ': ' + esc(v.error) : ''}${!v || v.ok ? '' : ' · ' + esc(what)}</span>`;
$('sources').innerHTML = [one('box', s.box, 'collect.mjs on the box, systemd timer every 30 s'), one('mac', s.mac, 'push.mjs on the Mac, launchd every 60 s'), one('pcs', s.pcs, 'relay intake read by push.mjs'), `<span>${esc(D._feed || '')}: fetched ${esc(ago(D._fetched))}, written ${t(D.generated_at)} by ${esc(D.generated_by || '?')}</span>`, D.box && D.box.log ? `<span>log ${D.box.log.present ? `${D.box.log.total} lines${D.box.log.bad_lines ? `, ${D.box.log.bad_lines} unreadable` : ''}` : 'not written yet'}</span>` : ''].join('');
}
function apply(d) {
D = d; D._fetched = new Date().toISOString();
const age = (Date.now() - Date.parse(d.generated_at)) / 1000;
const live = $('live'); live.className = 'live' + (age > 900 ? ' down' : age > 420 ? ' stale' : '');
const u = ukTime(d.generated_at);
$('stamp').innerHTML = `${age > 420 ? 'stale: ' : ''}written ${t(d.generated_at)} (${esc(ago(d.generated_at))})${d._feed ? ' · ' + esc(d._feed) : ''}`;
renderServer(d.box, d.sources && d.sources.box); renderCrew(); renderNow(); renderQueue(); renderDone(); renderHeadline(); renderAnalytics(); renderSources();
}
const LIVE_URL = 'https://build.igneum.network/workers.json'; // the box itself, 30 s fresh (Caddy, read-only); the folder copy is up to 5 min behind
let skipLive = 0, feed = '';
async function load() {
let d = null, err = null;
if (!demo && skipLive <= 0) {
try { const r = await fetch(`${LIVE_URL}?t=${Date.now()}`, { cache: 'no-store', signal: AbortSignal.timeout(4000) }); if (!r.ok) throw new Error(`http ${r.status}`); d = await r.json(); feed = 'live from the box'; }
catch (e) { skipLive = 4; err = e; } // the box not answering: use the edge copy for the next minute before trying again
} else skipLive--;
if (!d) {
try { const file = demo ? 'workers.demo.json' : 'workers.json'; const r = await fetch(`${base}/${file}?t=${Date.now()}`, { cache: 'no-store' }); if (!r.ok) throw new Error(`http ${r.status}`); d = await r.json(); feed = demo ? 'demo document' : 'edge copy, up to 5 min behind the box'; }
catch (e) { $('live').className = 'live down'; $('stamp').textContent = `workers.json not reachable (${e && e.message || e})`; if (!D) { $('now').innerHTML = $('queue').innerHTML = `<div class="empty">No workers.json next to this page yet and the box does not answer. The Mac pusher writes the copy every 60 s and the fleet deploy carries it within 5 min.</div>`; } return; }
}
d._feed = feed; apply(d);
}
await load(); setInterval(load, 15000); setInterval(tick, 1000); setInterval(() => { if (D) { renderSources(); } }, 10000);
</script>
</body></html>