399 lines
46 KiB
HTML
399 lines
46 KiB
HTML
<!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 + .server { margin-top: 12px; }
|
||
.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>
|
||
|
||
<div id="servers"></div>
|
||
|
||
<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>Background <span id="bg-sub">idle cores, yields to builds</span></h2>
|
||
<div class="lane" id="background"></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, classLine, fmtDur, fmtDurShort, fmtBytes, fmtBps, fmtUptime, ukTime, ago, headline } from './shape.js?v=3';
|
||
const $ = id => document.getElementById(id);
|
||
const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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' : '';
|
||
|
||
const FEEDS = { 'igneum-build-1': 'https://build.igneum.network/workers.json', 'igneum-build-2': 'https://build-2.igneum.network/workers.json', 'igneum-build-3': 'https://build-3.igneum.network/workers.json' };
|
||
const buildHeld = b => (b.slots && b.slots.held || []).filter(h => /^build-\d+$/.test(h.slot)).length;
|
||
const boxesOf = d => (Array.isArray(d.boxes) && d.boxes.length ? d.boxes : d.box ? [{ ...d.box, source: d.sources && d.sources.box, headline: d.headline }] : []);
|
||
function serverSection(b, i) {
|
||
const id = `srv${i}`;
|
||
if (!b || !b.cores) return `<section class="server" aria-label="${esc(b && b.name || 'build server')}"><div class="head"><div><div class="name">${esc(b && b.name || 'build server')}<small>build server</small></div><div class="facts"><span>${esc(b && b.source && b.source.error || 'no facts from this box yet')}</span></div></div></div><div class="cores">${Array.from({ length: 96 }, () => '<i style="--h:2%"></i>').join('')}</div><div class="corelabel"><span>no sample</span><span></span></div></section>`;
|
||
const per = (b.cpu && b.cpu.per_core) || []; const n = Math.max(per.length, b.cores || 96);
|
||
const temps = (b.temps || []).map(x => `${esc(x.name)} <b>${x.c.toFixed(0)}°</b>`);
|
||
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;
|
||
const stale = b.source && !b.source.ok;
|
||
return `<section class="server ${b.running && b.running.length ? 'hot' : ''}" aria-label="${esc(b.name)}">
|
||
<div class="head"><div><div class="name">${esc(b.name)}<small>${esc(b.os || 'build server')}</small></div><div class="facts">${[`<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('')}</div></div>
|
||
<div class="facts">${[...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)}" ${stale ? 'style="color:var(--warn)"' : ''}>${stale ? 'STALE, ' : ''}read ${esc(ago(b.collected_at))}</span>`].filter(Boolean).map(x => `<span>${x}</span>`).join('')}</div></div>
|
||
<div class="cores" style="grid-template-columns:repeat(${n > 64 ? 48 : n > 32 ? 32 : n}, 1fr)" aria-label="${n} cores, busy share per core">${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('')}</div>
|
||
<div class="corelabel"><span>${n} cores, ${b.cpu ? b.cpu.cores_busy : 0} busy</span><span>${b.cpu ? `${b.cpu.busy_pct}% of the box over the last second, ${b.compilers || 0} compilers running` : ''}</span></div>
|
||
<div class="gauges">
|
||
<div class="g">${arc(b.cpu ? b.cpu.busy_pct : 0)}<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 ? (buildHeld(b) / Math.max(1, b.slots.count)) * 100 : 0, 'good')}<div><div class="l">Build slots</div><div class="v">${b.slots ? `${buildHeld(b)}/${b.slots.count}` : '–'}</div><div class="s">${b.queue && b.queue.length ? `${b.queue.length} waiting${b.queue.some(q => q.priority === 'gate') ? ', a gate first' : ''}` : 'nobody waiting'}</div></div></div>
|
||
</div></section>`;
|
||
}
|
||
function renderServers() { const bx = boxesOf(D); $('servers').innerHTML = bx.length ? bx.map(serverSection).join('') : serverSection(null, 0); }
|
||
|
||
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 mac = D.mac, pcs = (D.pcs && D.pcs.machines) || [];
|
||
const cards = [];
|
||
for (const bx of boxesOf(D)) {
|
||
if (bx && bx.cores) cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx.name)}</div><div class="meta">Hetzner, ${bx.cores} threads, ${esc(fmtBytes((bx.mem && bx.mem.total_kb || 0) * 1024))}, RAID 1 NVMe</div></div>${pill(bx.running && bx.running.length ? 'building' : 'idle', bx.running && bx.running.length ? 'running' : '')}</div><div class="doing">${bx.running && bx.running.length ? bx.running.map(r => esc(`${r.worktree || '?'}: ${kindLabel(r.kind)}`)).join('<br>') : 'Slot free. The next build-remote.sh or cross-remote.sh routed here takes it.'}</div>${slotBar(bx.slots, bx.slots ? bx.slots.count : 1)}<div class="foot"><span>${bx.recent ? bx.recent.length : 0} builds logged</span><span>${esc(ago(bx.collected_at))}</span></div></div>`);
|
||
else cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx && bx.name || 'build server')}</div><div class="meta">build server</div></div>${pill('unreachable', 'error')}</div><div class="doing">${esc(bx && bx.source && bx.source.error || 'no facts from this 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 bx of boxesOf(D)) for (const r of bx.running || []) out.push({ where: bx.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 || '', classLine(r)].filter(Boolean).join(' · ') });
|
||
for (const bx of boxesOf(D)) for (const h of (bx.slots && bx.slots.held || []).filter(h => h.no_line && !(bx.running || []).some(r => r.slot === h.slot))) out.push({ where: bx.name, slot: h.slot, title: 'held, no holder line', sub: 'the slot is flock-held but its file is empty (the holder wrote no line)', cmd: '', started_at: null, kind: null, extra: '' });
|
||
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); } }
|
||
|
||
const BG_PRIORITY = { 'pow-fuzz': 1, 'sync-fuzz': 2, 'sim-sweeps': 3, 'model-sweeps': 4, 'clippy-audit': 5 };
|
||
const BG_LABEL = { 'pow-fuzz': 'igneum-pow fuzz', 'sync-fuzz': 'sync-request fuzz', 'sim-sweeps': 'GHOSTDAG + finality sweeps', 'model-sweeps': 'N-ladder + chip model', 'clippy-audit': 'clippy + audit per branch' };
|
||
function renderBackground() {
|
||
const bg = D.background;
|
||
const el = $('background'); const sub = $('bg-sub');
|
||
if (!bg || (bg.meta && bg.meta.ok === false)) {
|
||
sub.textContent = 'idle cores, yields to builds';
|
||
el.innerHTML = `<div class="empty">${esc((bg && bg.meta && bg.meta.error) || 'The capacity layer (infra/build-server/capacity) has not written capacity.json yet. It starts with igneum-capacity.service and writes within one slice.')}</div>`;
|
||
return;
|
||
}
|
||
const stale = bg.meta && bg.meta.stale;
|
||
const ctl = bg.state || 'unknown';
|
||
sub.innerHTML = `${esc(bg.host || 'igneum-build-1')} · controller <b class="${ctl === 'paused' || ctl === 'waiting' ? 'warn' : ''}">${esc(ctl)}</b>${bg.paused_reason ? ' · ' + esc(bg.paused_reason) : ''}${bg.current_job ? ' · on ' + esc(bg.current_job) : ''}${stale ? ' · <b class="no">stale</b>' : ''}`;
|
||
const jobs = bg.jobs || {};
|
||
const names = Object.keys(jobs).sort((a, b) => (jobs[a].priority || BG_PRIORITY[a] || 9) - (jobs[b].priority || BG_PRIORITY[b] || 9));
|
||
if (!names.length) { el.innerHTML = `<div class="empty">No jobs reported yet.</div>`; return; }
|
||
el.innerHTML = names.map(n => {
|
||
const j = jobs[n]; const pr = j.priority || BG_PRIORITY[n] || '';
|
||
const running = j.state === 'running' || j.state === 'ran';
|
||
const panics = j.counters && j.counters.panics;
|
||
const kindCls = panics ? 'error' : running ? 'running' : '';
|
||
const c = j.counters || {};
|
||
const nums = Object.entries(c).filter(([k]) => k !== 'panics').map(([k, v]) => `${esc(k)} ${esc(String(v))}`).join(' · ');
|
||
return `<div class="job"><div class="top"><div><div class="title">${esc(BG_LABEL[n] || n)}</div><div class="where">priority ${esc(String(pr))} · ${esc(n)}${n === D.background.current_job ? ' · now' : ''}</div></div>${pill(j.state || 'idle', kindCls)}</div><div class="cmd" title="${esc(j.summary || '')}">${esc(j.summary || '')}</div>${nums ? `<div class="cmd" style="opacity:.8">${nums}</div>` : ''}<div class="foot"><span>${panics ? `<b class="no">${esc(String(panics))} saved</b>` : '0 panics'}${j.out ? ' · ' + esc(String(j.out).replace(/^.*\/out\//, 'out/')) : ''}</span><span>${esc(ago(j.updated_at))}</span></div></div>`;
|
||
}).join('');
|
||
}
|
||
function renderQueue() {
|
||
const items = [];
|
||
for (const bx of boxesOf(D)) for (const w of bx.queue || []) items.push({ where: bx.name, title: w.label ? kindLabel(w.kind) : 'a build', sub: w.label ? `${w.worktree || ''}${w.crate ? '/' + w.crate : ''} ${w.command || ''}`.trim() : 'label unknown: no wait file for this flock', since: w.since, wait: w.waiting_s, gate: w.priority === 'gate', extra: [w.agent || '', classLine(w)].filter(Boolean).join(' · ') });
|
||
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${i.gate ? ' running' : ''}"><div class="top"><div><div class="title">${esc(i.title)}</div><div class="where">${esc(i.where)}${i.extra ? ' · ' + esc(i.extra) : ''}</div></div>${i.gate ? pill('gate queued', 'kind') : 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 bx of boxesOf(D)) for (const b of bx.recent || []) rows.push({ key: b.id, where: bx.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(boxesOf(D).flatMap(bx => bx.recent || []), { limit: 600 }); 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 bx = boxesOf(D).filter(b => b.headline); const rows = bx.length ? bx.flatMap(b => headline(b.headline).map(r => ({ ...r, label: bx.length > 1 ? `${r.label} · ${b.name}` : r.label }))) : 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 = boxesOf(D).flatMap(bx => (bx.recent || []).map(b => ({ ...b, host: b.host || bx.name })));
|
||
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 = [...boxesOf(D).map(b => one(b.name || 'box', b.source || 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>`, ...boxesOf(D).filter(b => b.log).map(b => `<span>${esc(b.name)} log ${b.log.present ? `${b.log.total} lines${b.log.bad_lines ? `, ${b.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) : ''}`;
|
||
const el = $('servers'); if (el && !el.innerHTML) el.innerHTML = '';
|
||
renderServers(); renderCrew(); renderNow(); renderQueue(); renderBackground(); renderDone(); renderHeadline(); renderAnalytics(); renderSources();
|
||
}
|
||
let skipLive = 0, feed = '';
|
||
async function fetchJson(url, ms) { const r = await fetch(`${url}?t=${Date.now()}`, { cache: 'no-store', signal: AbortSignal.timeout(ms) }); if (!r.ok) throw new Error(`http ${r.status}`); return r.json(); }
|
||
async function load() {
|
||
let edge = null, d = null;
|
||
try { const file = demo ? 'workers.demo.json' : 'workers.json'; edge = await fetchJson(`${base}/${file}`, 8000); }
|
||
catch (e) { if (!D && (demo || skipLive > 0)) { $('live').className = 'live down'; $('stamp').textContent = `workers.json not reachable (${e && e.message || e})`; } }
|
||
if (demo) { d = edge; feed = 'demo document'; }
|
||
else if (skipLive <= 0) {
|
||
// the boxes the edge copy knows (or build-1 alone before the first push), each from its own Caddy feed, 4 s each, in parallel
|
||
const names = edge ? boxesOf(edge).map(b => b.name).filter(Boolean) : ['igneum-build-1'];
|
||
const results = await Promise.all(names.map(n => FEEDS[n] ? fetchJson(FEEDS[n], 4000).then(j => ({ n, j })).catch(() => ({ n, j: null })) : Promise.resolve({ n, j: null })));
|
||
const liveOnes = results.filter(r => r.j);
|
||
if (liveOnes.length) {
|
||
const first = liveOnes[0].j;
|
||
const boxes = results.map(r => r.j ? { ...r.j.box, headline: r.j.headline, source: { ok: true, at: r.j.generated_at } } : (edge && boxesOf(edge).find(b => b.name === r.n)) || { name: r.n, source: { ok: false, error: 'no live feed and no edge copy for this box' } });
|
||
d = { ...first, boxes, mac: first.mac || (edge && edge.mac), pcs: first.pcs || (edge && edge.pcs), baselines: first.baselines || (edge && edge.baselines), sources: { ...(first.sources || {}), box: boxes[0].source } };
|
||
feed = liveOnes.length === results.length ? `live from ${liveOnes.length === 1 ? 'the box' : liveOnes.length + ' boxes'}` : `live from ${liveOnes.map(r => r.n).join(', ')}, edge copy for the rest`;
|
||
} else skipLive = 4; // nothing answered: use the edge copy for the next minute before trying again
|
||
} else skipLive--;
|
||
if (!d) { d = edge; feed = 'edge copy, up to 5 min behind the box'; }
|
||
if (!d) { if (!D) { $('now').innerHTML = $('queue').innerHTML = `<div class="empty">No workers.json next to this page yet and no box answers. 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>
|