diff --git a/docs/bugs.md b/docs/bugs.md index 3d3d2ebc1..2fbc1cff7 100644 --- a/docs/bugs.md +++ b/docs/bugs.md @@ -20,7 +20,7 @@ shard run reported as exit 0, 7a7e873). | 4 Oct 2026 | the Mac card read 0.0 MH/s for a minute while its blocks were still accepted (after the stale mark shipped) | STALE_S 120 s left no margin: one missed 60 s upload (a 120 s gap at 19:45:59 UTC) plus a 30 s STATUS age plus the 10 s cache | STALE_S 180 s (one missed upload is not stale; PC 2's real case was a 1,860 s gap) | test: 150 s is fresh, 240 s stale | | 4 Oct 2026 | a machine stopped on purpose (Sam's Mac, 14:47 UTC) read "silent 4h" on the console, the same as a crash or a lost network | the app logs `quit: stopping the miners, then the node` and `stopped` and uploads once more before it exits (so it does report), but the console never read those lines | 4d208c9: `parseAppTail` (now in `relay/lib/parse.mjs`) sets `stopped {at, reason quit\|update}` when a quit or OTA-install line has no status line after it; the card says "stopped (quit) N ago" with a grey stripe, `tools/console.mjs` says STOPPED | unit test: quit, update, running, and a quit followed by a later status line; live: five running machines show no false stop; a live stopped case is pending the next real quit | | 4 Oct 2026 | `publish-manifest.sh --deploy` called the live manifest "verified" after one signature check: any validly signed manifest, including the previous version still at the edge, passed; a failed deploy was hidden by `\|\| true` | one-shot check, signature only, no byte compare | `verify_live_manifest`: up to `--tries` checks 5 s apart, byte-identical to the folder's file, then the signature, each failure named; `--verify-only` runs just the check; a failed deploy stops first (the ship tool's own verify step already compared bytes and version, so a cut through `tools/ship-app.mjs` was covered; a hand publish was not) | finished: `--verify-only --tries 2` against the live 0.3.3 (try 1); failed: a local server with an altered copy ("differs", both stamps named) and a closed port ("is not reachable") | -| 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)` | that line is the OTA state (`current`, coloured as good) unless a newer `OTA:` line follows | test: current alone, a newer OTA line wins, an older one does not; live card after the deploy | +| 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 | ## Open diff --git a/relay/lib/parse.mjs b/relay/lib/parse.mjs index 2f605176c..f2f64938d 100644 --- a/relay/lib/parse.mjs +++ b/relay/lib/parse.mjs @@ -81,10 +81,26 @@ export function parseAppTail(tail) { // OTA: the newest line decides the state shown on the card const ota = lastMatch(lines, /^\d+(?:\.\d+)? (?:\[\w+\] )?(OTA: .+)$/); if (ota) { const t = ota[1]; a.ota = { text: t.slice(0, 220), at: stampOf(ota[0]), state: /waiting for administrator approval|approval not given/.test(t) ? 'waiting for approval' : /deferred/.test(t) ? 'deferred' : /updated to|installed|applied/.test(t) ? 'updated' : /download/.test(t) ? 'downloading' : 'note' }; } - // After an update the new run's log has no "OTA:" line, only the check that found itself current (4 October 2026, - // 20:33: PC 2 came back on 0.3.4 and its card said nothing about it); that check is the OTA state until a newer OTA line - const current = lastMatch(lines, /^(\d+)(?:\.\d+)? update check: (\S+) is current \(manifest (\S+)\)$/); - if (current && (!ota || Number(current[1]) >= Number((/^(\d+)/.exec(ota[0]) || [])[1]))) a.ota = { text: `update check: ${current[2]} is current (manifest ${current[3]})`, at: stampOf(current[0]), state: 'current', version: current[2] }; + // The app's other update lines (app/igneum-app/src/ota.rs): the newest of them, the OTA line above and the plain + // "update check" decides the state shown on the card. 4 October 2026, 20:25: the Mac had downloaded, verified and + // staged 0.3.4 ("update: ... downloaded and verified", "[ok] ... is ready; it installs at the next safe moment") and + // its card said "0.3.3 is current" from a check 70 minutes older; a finished update's new run says only "is current". + const SHAPES = [ + [/^(\d+)(?:\.\d+)? \[info\] Igneum Miner (\S+) is available: downloading/, 'downloading'], + [/^(\d+)(?:\.\d+)? update: .*Igneum-Miner-(\d+\.\d+\.\d+)\S* downloaded and verified/, 'downloaded'], + [/^(\d+)(?:\.\d+)? \[ok\] Igneum Miner (\S+) is ready; it installs at the next safe moment/, 'staged'], + [/^(\d+)(?:\.\d+)? \[info\] installing Igneum Miner (\S+)/, 'installing'], + [/^(\d+)(?:\.\d+)? update to (\S+) complete/, 'updated'], + [/^(\d+)(?:\.\d+)? update: (\S+) is marked failed/, 'failed'], + [/^(\d+)(?:\.\d+)? update check: (\S+) is published but has no \S+ build yet/, 'no build'], + [/^(\d+)(?:\.\d+)? update check: (\S+) is current \(manifest \S+\)/, 'current'], + ]; + let best = a.ota ? { at: Number((/^(\d+)/.exec(ota[0]) || [])[1]) } : null; + for (const [re, state] of SHAPES) { + const m = lastMatch(lines, re); + if (m && (!best || Number(m[1]) >= best.at)) best = { at: Number(m[1]), ota: { text: strip(m[0]).replace(/^\[\w+\] /, '').slice(0, 220), at: stampOf(m[0]), state, version: m[2] } }; + } + if (best && best.ota) a.ota = best.ota; const pc = lastMatch(lines, /^\d+(?:\.\d+)? (?:\[\w+\] )?(?:GPU )?(power cap.*)$/i); if (pc) a.power_cap = { text: pc[1].slice(0, 220), at: stampOf(pc[0]), ok: !/NOT applied|nothing to read back/.test(pc[1]) }; const jf = lastMatch(lines, /^\d+(?:\.\d+)? jobs: (.+)$/); diff --git a/relay/test/parse.test.mjs b/relay/test/parse.test.mjs index 326cae60d..12a48adc4 100644 --- a/relay/test/parse.test.mjs +++ b/relay/test/parse.test.mjs @@ -64,14 +64,21 @@ test('app tail: a clean quit after the last status line is "stopped (quit)", an assert.equal(parseAppTail(resumed).stopped, undefined); }); -test('app tail: after an update the "update check: X is current" line is the OTA state; an OTA line that is newer wins', async () => { +test('app tail: the newest update line decides the OTA state: staged beats an older check, a finished run says current, a newer OTA line wins', async () => { const { parseAppTail } = await import('../lib/parse.mjs'); - const after = ['1791146000 IGNEUM-APP version=0.3.4 machine=1ccfe586 platform=windows node=igneumd/2.1.0', '1791146426 update check: 0.3.4 is current (manifest 0.3.4)', - '1791146430 status: accepted 45 blocks (45 this run), 122.00 MH/s, mining | node 44306 blocks, 2 peers, synced | up 00:08:00'].join('\n'); + const st = n => `${n} status: accepted 1 blocks (1 this run), 1.00 MH/s, mining | node 1 blocks, 1 peers, synced | up 00:01:00`; + const mac = ['1791141355 update check: 0.3.3 is current (manifest 0.3.3)', st(1791145000), + '1791145529 [info] Igneum Miner 0.3.4 is available: downloading (39 MB). Finality rule v3', + '1791145533 update: /Users/x/Library/Application Support/Igneum/app/updates/Igneum-Miner-0.3.4.dmg downloaded and verified', + '1791145562 update: staged bundle digest 2b640b8c', '1791145562 [ok] Igneum Miner 0.3.4 is ready; it installs at the next safe moment', st(1791146000)].join('\n'); + const m = parseAppTail(mac); + assert.deepEqual([m.ota.state, m.ota.version, m.ota.at], ['staged', '0.3.4', new Date(1791145562000).toISOString()]); + const after = ['1791146000 IGNEUM-APP version=0.3.4 machine=1ccfe586 platform=windows node=igneumd/2.1.0', '1791146426 update check: 0.3.4 is current (manifest 0.3.4)', st(1791146430)].join('\n'); const a = parseAppTail(after); - assert.deepEqual([a.ota.state, a.ota.version, a.ota.at], ['current', '0.3.4', new Date(1791146426000).toISOString()]); - const newer = after + '\n1791146500 OTA: 0.3.5 downloaded, waiting for this machine\'s minute'; - assert.notEqual(parseAppTail(newer).ota.state, 'current'); - const older = '1791146400 OTA: 0.3.4 installed\n' + after; - assert.equal(parseAppTail(older).ota.state, 'current'); + assert.deepEqual([a.ota.state, a.ota.version], ['current', '0.3.4']); + assert.notEqual(parseAppTail(after + "\n1791146500 OTA: 0.3.5 downloaded, waiting for this machine's minute").ota.state, 'current'); + assert.equal(parseAppTail('1791146400 OTA: 0.3.4 installed\n' + after).ota.state, 'current'); + assert.equal(parseAppTail(mac + '\n1791146600 update: 0.3.4 is marked failed; it will not be applied again by itself').ota.state, 'failed'); + assert.equal(parseAppTail(mac + '\n1791146600 update to 0.3.4 complete (from 0.3.3); keeping the previous version for a rollback').ota.state, 'updated'); + assert.equal(parseAppTail(st(1)).ota, undefined); });