fix: fresh-ISO feedback bug-bash — onboarding, status truthfulness, recovery, kiosk, logs
Fixes from real fresh-install feedback (Framework node .81) + its log bundle:
Backend:
- websocket: subscribe before initial snapshot — broadcasts in the gap were
silently lost, stranding clients on stale state until a hard refresh
(the "everything needs ctrl-r" bug: My Apps stuck Loading, App Store
stuck Checking, containers-scanned never arriving)
- crash recovery: check the crash marker BEFORE writing our own PID —
recovery had never run on any node (always saw its own PID and skipped);
PID-reuse guard via /proc cmdline
- boot status: pending-boot-starts registry (recovery, stack recovery,
reconciler, adoption) — scanner overlays queued-but-down apps as
Restarting instead of Stopped after a reboot; scanner-authored
Restarting resolves immediately on a settled scan (no transitional wedge)
- install deps: bounded wait (36x5s) when a dependency is installed but
still starting ("Waiting for Bitcoin to start…") instead of instant
rejection; dependency-gate rejections remove the optimistic entry (no
phantom Stopped tile) and surface as a notification
- seed backup: auth.setup persists the onboarding mnemonic as the
encrypted seed backup (reveal previously failed on EVERY node — nothing
ever wrote master_seed.enc); seed.restore stashes too; error sanitizer
lets seed/2FA errors through instead of "Check server logs"
- lnd: bitcoind.rpchost resolved from the running Bitcoin variant
(hardcoded bitcoin-knots broke Core nodes); manifest uses derived_env
- bitcoin status: clean human message for connection-reset/startup; raw
URLs + os-error chains no longer reach the app card
- fedimint-clientd: chown /var/lib/archipelago/fmcd to 1000:1000 (root-
created dir crash-looped the rootless container, EACCES) — first-boot
script + pre-start self-heal
- log volume (>1GB/day on a day-old node): journald caps drop-in (ISO +
bootstrap self-heal), bitcoind -printtoconsole=0 everywhere (90% of the
journal was IBD UpdateTip spam), tracing default debug→info
Frontend:
- Login: Enter advances to confirm field then submits; submit always
clickable with inline errors (was silently disabled on mismatch);
Restart Onboarding needs a confirming second click (the mismatch →
"onboarding restarted" trap)
- sync store: 30s state reconciliation + refetch on re-entrant connect;
20s containers-scanned escape hatch so Checking can never show forever;
fresh empty node reaches the real "no apps yet" state
- intro video: CRF20 re-encode (SSIM 0.988) + faststart — moov was at EOF
so playback needed the full 15MB first (the intro lag)
- backgrounds: 10 heaviest JPEGs → WebP q90 (9.4MB→6.6MB); 7 stayed JPEG
(WebP larger on noisy sources)
- Web5ConnectedNodes: drop unused template ref that failed vue-tsc -b
ISO/kiosk:
- nginx: /assets/ 404s no longer cached immutable for a year; HTTPS block
gained the missing /assets/ location (served index.html as images)
- kiosk: launcher/service spliced from configs/ at ISO build (stale
heredoc force-disabled GPU); MemoryHigh/Max 1200/1500→2200/2800M (kiosk
rode the reclaim throttle = the lag); firmware-intel-graphics +
firmware-amd-graphics (trixie split DMC blobs out of misc-nonfree)
Verified: cargo test 898/898 green, npm run build green with dist
contents confirmed (webp refs, lnd.png, faststart video, new strings).
Handover for ISO build + deploy: docs/HANDOVER-2026-07-02-iso-feedback.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8256fde1a6
commit
c375ecc441
@@ -1203,6 +1203,21 @@ fn merge_preserving_transitional(
|
||||
}
|
||||
}
|
||||
|
||||
/// Package ids whose `Restarting` state was written by the scanner's
|
||||
/// pending-boot-start overlay (not by an RPC restart task). For these, the
|
||||
/// scan is the owner: once podman reports a settled state and the id is no
|
||||
/// longer queued for a boot start, the fresh state wins immediately instead
|
||||
/// of being preserved for the transitional-stuck timeout.
|
||||
static SCANNER_RESTARTING: std::sync::LazyLock<std::sync::Mutex<std::collections::HashSet<String>>> =
|
||||
std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashSet::new()));
|
||||
|
||||
fn take_scanner_restarting(id: &str) -> bool {
|
||||
SCANNER_RESTARTING
|
||||
.lock()
|
||||
.map(|mut set| set.remove(id))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_podman_scan_timeout(error: &anyhow::Error) -> bool {
|
||||
let msg = format!("{:#}", error);
|
||||
msg.contains("podman ps") && msg.contains("timed out")
|
||||
@@ -1223,6 +1238,25 @@ async fn scan_and_update_packages(
|
||||
pkg.state = crate::data_model::PackageState::Stopped;
|
||||
pkg.exit_code = None;
|
||||
}
|
||||
// A down container that boot recovery / the reconciler is queued to
|
||||
// start is "Restarting", not "Stopped" — after a reboot the sequential
|
||||
// recovery pass can take minutes to reach heavyweights, and telling
|
||||
// the user their app stopped when it's about to come back is wrong.
|
||||
// Ids overlaid here are recorded in SCANNER_RESTARTING so the merge
|
||||
// below knows this Restarting is scanner-authored (resolve it as soon
|
||||
// as podman reports a settled state) and not owned by an RPC restart
|
||||
// task (whose transitional state must be preserved).
|
||||
if matches!(
|
||||
pkg.state,
|
||||
crate::data_model::PackageState::Stopped | crate::data_model::PackageState::Exited
|
||||
) && crate::crash_recovery::is_pending_boot_start(id)
|
||||
{
|
||||
pkg.state = crate::data_model::PackageState::Restarting;
|
||||
pkg.exit_code = None;
|
||||
if let Ok(mut set) = SCANNER_RESTARTING.lock() {
|
||||
set.insert(id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
normalize_reachable_package_health(&mut packages).await;
|
||||
|
||||
@@ -1273,6 +1307,19 @@ async fn scan_and_update_packages(
|
||||
absence_tracker.remove(id);
|
||||
let existing = merged.get(id);
|
||||
let overwrite = match existing {
|
||||
// Scanner-authored Restarting (the pending-boot-start overlay)
|
||||
// resolves as soon as the fresh scan reports anything else: the
|
||||
// scan is its owner — no RPC task will ever write a final state
|
||||
// back. Without this, a successfully recovered container would
|
||||
// sit wedged in "Restarting" until the 20-minute stuck timeout.
|
||||
Some(existing_entry)
|
||||
if existing_entry.state == crate::data_model::PackageState::Restarting
|
||||
&& pkg.state != crate::data_model::PackageState::Restarting
|
||||
&& take_scanner_restarting(id) =>
|
||||
{
|
||||
transitional_since.remove(id);
|
||||
true
|
||||
}
|
||||
Some(existing_entry) if is_transitional(&existing_entry.state) => {
|
||||
let entered = *transitional_since.entry(id.clone()).or_insert(now);
|
||||
let timeout = transitional_stuck_timeout(&existing_entry.state);
|
||||
|
||||
Reference in New Issue
Block a user