// Igneum console: one function, dispatched on ?fn=, reached through the rewrite /r//c/. // Auth: the token in the path (or x-relay-token), or the intake key in x-igneum-key. Nothing else. No secret // reaches the client: DL_TOKEN (the downloads folder) is read here from the project env. // Every GET answer is cached 10 s in the function instance. // GET machines one card per machine from the log intake (miner_logs): node, cards, telemetry, faults // GET jobs igneum-jobs.json from the downloads host + per-machine status from run_id job-- // GET job?run_id= the newest upload of that job run, in full // GET builds OTA manifest, CI json, build posts, downloads listing (synced by tools/console.mjs sync-dl) // GET chain igneum.network/api/live trimmed + the Hetzner results item // GET log?limit=&since= work-log items (kinds log, build, note), newest first // GET results bench entries (synced from docs/bench-log.md) + the FUD ledger counts // GET tuning?days=30&min=5 Ember Tune: the fleet priors per (card model, driver major, program class) from the // TUNE records in miner_logs (relay/lib/ember.mjs), with the sample counts and MH/W // POST post {kind,title,body,who,key?,meta?} one item; with key it upserts // POST sync {items:[...]} bulk upsert by key import { neon, authed, readJson, str, iso } from '../lib/relay.mjs'; import { kv, kvNum, lastMatch, FAULT, parseLabel, parseMinerTail, parseHeader, parseAppTail, STALE_S, markStale } from '../lib/parse.mjs'; import { seenList, pingState, PING_SILENT_S } from '../lib/wake.mjs'; import { parseRecords, aggregate } from '../lib/ember.mjs'; const json = (res, status, obj) => { res.status(status).setHeader('Content-Type', 'application/json; charset=utf-8'); res.end(JSON.stringify(obj)); }; const CACHE_MS = 10_000; const cache = new Map(); async function cached(key, fn) { const hit = cache.get(key); if (hit && Date.now() - hit.at < CACHE_MS) return hit.value; const value = await fn(); cache.set(key, { at: Date.now(), value }); return value; } const DL_BASE = () => process.env.DL_TOKEN ? `https://dl.igneum.network/dl/${process.env.DL_TOKEN}` : ''; const LIVE_URL = process.env.LIVE_URL || 'https://igneum.network/api/live'; const NAMES = { ae432dc7: 'PC 1', '1ccfe586': 'PC 2', '3a9bf309': "Sam's Mac" }; const NAMED = new Set(Object.values(NAMES).concat('Mac')); const HIDE_AFTER_S = 1800; // ids other than the named machines drop off 30 min after their last upload (test runs) const SILENT_S = 180; const num = v => (v === null || v === undefined || v === '' ? null : Number(v)); async function fetchJson(url, ms = 6000) { const r = await fetch(url, { signal: AbortSignal.timeout(ms), headers: { accept: 'application/json' } }); if (!r.ok) return null; return r.json(); } // ---- machines --------------------------------------------------------------------------------------------------- function parseNodeTail(tail) { const lines = tail.split('\n'); const n = {}; let m; if ((m = lastMatch(lines, /Processed (\d+) blocks and (\d+) headers in the last/))) { n.blocks_10s = Number(m[1]); n.headers_10s = Number(m[2]); } if ((m = lastMatch(lines, /Connection manager: has (\d+)\/(\d+) outgoing/))) { n.peers = Number(m[1]); n.peers_wanted = Number(m[2]); } if ((m = lastMatch(lines, /\(daa (\d+)/))) n.daa = Number(m[1]); if ((m = lastMatch(lines, /checkpoint (\d+) determined: block [0-9a-f]+ \(blue score (\d+), daa (\d+)\)/))) { n.checkpoint = Number(m[1]); n.blue_score = Number(m[2]); if (!n.daa || Number(m[3]) > n.daa) n.daa = Number(m[3]); } if ((m = lastMatch(lines, /IBD/))) n.ibd_seen = true; if ((m = lastMatch(lines, /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+[+-]\d{2}:\d{2})/))) n.last_line_at = iso(m[1].replace(' ', 'T')); if ((m = lastMatch(lines, /igneumd\/(\S+)/))) n.version = m[1]; const errs = lines.filter(l => / \[ERROR\] | \[WARN \] /.test(l)); n.warnings = errs.length; n.last_warning = errs.length ? errs[errs.length - 1].replace(/^\S+ \S+ /, '').slice(0, 200) : null; return n; } // The app uploads at most the last 256 KiB of its log, so an update line scrolls out of the tail in about half an hour // on a PC that logs every block (4 October 2026: PC 1's "installing 0.3.4" left its card while the install was still // stuck). The last OTA state seen per machine and app run is kept in console_ota_memo and read back, for the same run, // when the tail has none; a new run (the app relaunched) starts clean. async function otaMemo(sql, groups) { try { await sql(`CREATE TABLE IF NOT EXISTS console_ota_memo (id8 text PRIMARY KEY, run_id text NOT NULL, ota jsonb NOT NULL, updated_at timestamptz NOT NULL DEFAULT now())`); const live = [...groups.values()].filter(g => g.app && !g.id.startsWith('legacy:')); if (!live.length) return; const rows = await sql(`SELECT id8, run_id, ota FROM console_ota_memo WHERE id8 = ANY($1)`, [live.map(g => g.id)]); const memo = new Map(rows.map(r => [r.id8, r])); for (const g of live) { const m = memo.get(g.id); if (g.app.ota) { if (!m || m.run_id !== g.app.run_id || JSON.stringify(m.ota) !== JSON.stringify(g.app.ota)) { await sql(`INSERT INTO console_ota_memo (id8, run_id, ota, updated_at) VALUES ($1, $2, $3::jsonb, now()) ON CONFLICT (id8) DO UPDATE SET run_id = EXCLUDED.run_id, ota = EXCLUDED.ota, updated_at = now()`, [g.id, g.app.run_id, JSON.stringify(g.app.ota)]); } } else if (m && m.run_id === g.app.run_id && m.ota) { g.app.ota = { ...m.ota, remembered: true }; } } } catch (e) { /* a convenience: the cards render without it */ } } async function machines(sql) { // node logs get a longer tail: the connection-manager (peers) line is rare on a busy node // the app log's tail is wide (400 KB, about three hours on a PC that logs every block) so a stuck update's lines do // not scroll out of the card: on 4 October 2026 PC 1's "installing 0.3.4" left the 60 KB window after 30 minutes const rows = await sql(`SELECT DISTINCT ON (label) label, machine, run_id, received_at, left(lines, 300) AS head, right(lines, CASE WHEN label LIKE 'nodelog-%' THEN 80000 WHEN label ~ '^(win|mac)-[0-9a-f]{8}$' THEN 400000 ELSE 20000 END) AS tail FROM miner_logs WHERE received_at > now() - interval '36 hours' ORDER BY label, received_at DESC`); const groups = new Map(); for (const r of rows) { const p = parseLabel(r.label, r.machine); if (!p) continue; const g = groups.get(p.id) || { id: p.id, platform: p.platform, hosts: new Set(), runs: new Set(), last_seen: null, node: {}, cards: [], app: null, labels: [], header: null }; const hd = parseHeader(r.head) || parseHeader(r.tail); if (hd) g.header = hd; g.hosts.add(r.machine.replace(/-[0-9a-f]{8}$/, '')); g.runs.add(r.run_id); const at = iso(r.received_at); if (!g.last_seen || at > g.last_seen) g.last_seen = at; g.labels.push({ label: r.label, at, run_id: r.run_id }); if (p.stream === 'node') { g.node = { ...parseNodeTail(r.tail), at, run_id: r.run_id }; } else if (p.stream === 'miner') g.cards.push({ label: r.label.replace(/^miner-/, ''), vendor: p.vendor, card: p.card, at, ...parseMinerTail(r.tail) }); else if (p.stream === 'app') g.app = { at, run_id: r.run_id, ...parseAppTail(r.tail) }; groups.set(p.id, g); } // node version lives in the first upload of a run (the tail may have lost the banner): one cheap query for the current runs const runIds = [...new Set([...groups.values()].flatMap(g => [...g.runs]))]; if (runIds.length) { const heads = await sql(`SELECT DISTINCT ON (run_id, label) run_id, label, left(lines, 3000) AS head FROM miner_logs WHERE run_id = ANY($1) ORDER BY run_id, label, received_at ASC`, [runIds]); for (const h of heads) { const g = [...groups.values()].find(x => x.runs.has(h.run_id)); if (!g) continue; const m = /igneumd\/(\S+)/.exec(h.head); if (m && h.label.startsWith('nodelog-') && !g.node.version) g.node.version = m[1]; const hd = parseHeader(h.head); if (hd && !g.header) g.header = hd; } } await otaMemo(sql, groups); // the job channel's ping (MF-11, 7 October 2026): every 0.3.21 app names itself on its wake long-poll; a machine whose // last poll is over PING_SILENT_S old reads "job channel silent since