fix: RC feedback round 2 — kiosk staleness, LND icon+retry, mesh detection surfacing

Kiosk (backgrounds + FIPS mismatch, one root cause each):
- kiosk service now also waits for the web-ui asset swap (probes a large
  bg asset, bounded 60s) — it launched mid-rsync and CSS background 404s
  are never refetched, hence blank backgrounds on first boot
- FipsNetworkCard polls every 15s instead of fetching once on mount
- PWA updates auto-apply on the kiosk (localStorage kiosk flag) — an
  unattended kiosk could never click 'Update Now' and ran stale bundles

LND App Store:
- catalog icon pointed at lnd.svg which doesn't exist (lnd.png does);
  store card also retries the sibling extension before the placeholder
- image pulls retry once after fast transient failures (<30s) — fresh
  first-boot networks reset connections; slow failures aren't retried
  so the UI never waits longer than before

Mesh detection:
- mesh.status now always reports device_present + detected_devices
  (previously only when the service wasn't running, so the UI couldn't
  tell 'no radio' from 'radio present, session connecting')
- Mesh page shows 'Mesh device detected' badge + 'Device detected —
  connecting' status for present-but-not-connected radios
- the 99-mesh-radio.rules udev rule was never staged into the ISO tree
  (installer searched for it and found nothing) — ship it at the media
  root and install it from install-to-disk.sh too

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-07-13 18:41:00 +01:00
co-authored by Claude Fable 5
parent ca89cb4196
commit 00a70c6193
11 changed files with 93 additions and 7 deletions
@@ -37,6 +37,21 @@ impl RpcHandler {
"receive_block_headers".into(),
config.receive_block_headers.into(),
);
// Raw serial-device presence, in BOTH branches. MeshStatus has no
// such field, so while the service was running the UI couldn't
// tell "no radio plugged in" from "radio present but the session
// can't open it yet" — both looked like device_connected=false.
if !obj.contains_key("detected_devices") {
let devices = mesh::detect_devices().await;
obj.insert("device_present".into(), (!devices.is_empty()).into());
obj.insert("detected_devices".into(), devices.into());
} else {
let present = obj
.get("detected_devices")
.and_then(|v| v.as_array())
.is_some_and(|a| !a.is_empty());
obj.insert("device_present".into(), present.into());
}
}
Ok(value)
}
@@ -1180,6 +1180,7 @@ impl RpcHandler {
// the fresh pull instead of showing stale bytes from a prior
// partial attempt.
self.set_install_progress(package_id, 0, 0).await;
let started = std::time::Instant::now();
match self
.pull_one_url_with_progress(url, *tls_verify, package_id, &user_tmp)
.await?
@@ -1190,6 +1191,24 @@ impl RpcHandler {
break;
}
false => {
// A fast failure (DNS miss / TCP reset while a fresh
// node's network is still warming up) is worth one quick
// retry — observed on first-boot installs where the
// second click succeeded. A slow failure already spent
// the 300s budget; retrying it just doubles the wait.
if started.elapsed() < std::time::Duration::from_secs(30) {
tracing::info!("Fast pull failure on {}, retrying once in 5s", url);
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
self.set_install_progress(package_id, 0, 0).await;
if self
.pull_one_url_with_progress(url, *tls_verify, package_id, &user_tmp)
.await?
{
tracing::info!("Pulled {} from {} (retry)", docker_image, url);
pulled_url = Some(url.clone());
break;
}
}
tracing::debug!(
"Pull attempt {}/{} failed for {}, trying next mirror",
i + 1,