diff --git a/docs/bugs.md b/docs/bugs.md index 931ba5f4..8a646deb 100644 --- a/docs/bugs.md +++ b/docs/bugs.md @@ -23,6 +23,7 @@ shard run reported as exit 0, 7a7e873). | 4 Oct 2026 | PC 2 came back on 0.3.4 (the canary) and its card's OTA state read "none" | the OTA state parsed only `OTA: …` lines, and the install lines sit in the previous run's log file; the new run only says `update check: 0.3.4 is current (manifest 0.3.4)` | 797a844 then 2nd commit: every update line the app logs (`is available: downloading`, `downloaded and verified`, `is ready; it installs at the next safe moment`, `installing`, `update to X complete`, `is marked failed`, `has no build yet`, `is current`) is parsed and the newest decides the state; the Mac's card had read "0.3.3 is current" from a 70-minute-old check while 0.3.4 was downloaded, verified and staged | test: staged beats the older check, a finished run reads current, a newer OTA line wins, failed and updated; live cards after the deploy | | 4 Oct 2026 | the live 0.3.3 Mac bundle's `Info.plist` said 0.3.2 while the engine reported 0.3.3 (Finder and Get Info showed the wrong version) | the 0.3.3 cut was by hand and the plist was not bumped | nothing to do: 0.3.4 was cut by `tools/ship-app.mjs`, which bumps the six version files, and its plist says 0.3.4 | `PlistBuddy` on the staged 0.3.4 bundle | | 4 Oct 2026 | 0.3.4 reports `igneumd/2.1.0-73fc2fd` on Windows and `-1b65132` on the Macs; neither is a fork commit (both are igneum-repo commits), so the suffix says nothing about the node source | the fork's `build-info/build.rs` looks for a `.git` directory; a worktree's `.git` is a file, so the walk climbs into the vendoring igneum repository and embeds its HEAD; then it reads `.git/HEAD` as a path, which a worktree lacks | fork branch `bugfix-build-info-worktree` (vendor/igneum-node-bughunt, commits 92e1b030 and its follow-up): the root is any `.git`, the HEAD file comes from `git rev-parse --absolute-git-dir`, the ref from `--git-common-dir`; for the main session to merge into devnet-v4 and finality-fixes (the fork has no remote) | `cargo build -p kaspa-build-info` in the fixed worktree embeds the fork's `92e1b030` and watches the worktree's HEAD; the unfixed devnet-v4 worktree embeds the outer repo's `ebce665` and watches the outer repo's files | +| 4 Oct 2026 | PC 1's card went from "installing 0.3.4" to "none" while the install was still stuck (the Windows helper bug, 0d123b3) | the app uploads at most the last 256 KiB of its log (262,144 chars per upload on PC 1) and a PC logs every block, so an update line leaves the parsed tail in about 30 min | 699d0d8: `console_ota_memo` keeps the last OTA state per machine and app run, written when a parse finds one and read back for the same run when the tail has none; a relaunch starts clean (0855c44 widened the SQL window first, which cannot help against the upload cap) | after the deploy all five machines carry memo rows and the cards read installing (PC 1), staged (PC 37ba0461), updated (the rest) | ## Open diff --git a/relay/api/console.mjs b/relay/api/console.mjs index 06275da4..7dcb5358 100644 --- a/relay/api/console.mjs +++ b/relay/api/console.mjs @@ -55,10 +55,37 @@ function parseNodeTail(tail) { 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 60000 ELSE 20000 END) AS tail + 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) { @@ -73,7 +100,7 @@ async function machines(sql) { 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, ...parseAppTail(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 @@ -86,6 +113,7 @@ async function machines(sql) { const hd = parseHeader(h.head); if (hd && !g.header) g.header = hd; } } + await otaMemo(sql, groups); const now = Date.now(); const out = [...groups.values()].map(g => { const id8 = g.id.startsWith('legacy:') ? null : g.id;