feat(machine): connectivity auto-recovery + on-screen Retry #82

Merged
padreug merged 3 commits from feat/connection-recovery into dev 2026-08-05 02:33:01 +00:00
Owner

Problem

A connectivity-type init failure (e.g. "No connected relays" when the box boots before the network is up) lands on the "ATM Unavailable" screen and stays there forever. Providing internet afterward does nothing:

  • App init is one-shot (App.vue onMounted try/catch → sets initError, never retries).
  • initError only self-clears for awaiting-fees; connectivity errors are terminal.
  • The nostr client reconnects with backoff — but only after a first successful connect, so it can't rescue a cold boot with no internet.
  • The watchdog can't help (renderer is alive and ponging), and systemd Restart=always can't help (the process doesn't exit).

Fix — layered, cheapest-first recovery (see ADR-002 amendment in this PR)

  1. App auto-recovery (first-line, no human). When the maintenance screen is a connectivity fault, the app watches for the browser online event (recovers immediately when the network returns) plus a 45s backoff safety net (covers online-but-relay-unreachable). Recovery = a renderer reload → re-runs init from a clean JS context (no leaked actors/subscriptions), preserving HAL in the main process and pairing//var/lib state.
  2. On-screen Retry — a kiosk-sized button on "ATM Unavailable" for an operator standing at the machine.
  3. SSH/NetBird stays as the last-resort remote plane — ADR-002 amended to state it is not the only/first-line recovery.

Scoped to connectivity errors only; does not hijack unpaired (pairing wizard), awaiting-fees (self-clears on the operator fee event), or maintenance (operator-set).

Changes

  • main.ts: new app:recover IPC → reloadRenderer() (resets secretsConsumed); hal:init made idempotent (reuse existing instance) so the recovery reload and the pre-existing watchdog crash-reload can't double-open the validator/dispenser serial ports — a latent-bug fix for the watchdog path too.
  • preload.ts / electron.d.ts: expose recoverApp().
  • App.vue: recovery scheduler (online + backoff) + kiosk Retry button.
  • docs/adr/002-*: amendment — app recovery first-line, SSH/NetBird last-resort.
  • flake.nix: expose nixosConfigurations.batm3-usb as a named config (extracted from the inline disk-image-batm3-usb) so a running stick's app can be updated in place via nix copy + switch-to-configuration — no reflash. Disk image builds from the same config.

Verification

  • vue-tsc typecheck clean.
  • Deployed in-place to the batm3 (switch-to-configuration test): service active, app:recover present in the bundle, boots to idle, Fully initialized.
  • Runtime recovery still to be exercised on hardware: boot/block the relay → confirm "ATM Unavailable" + Retry → restore network → confirm it self-heals.

🤖 Generated with Claude Code

## Problem A connectivity-type init failure (e.g. **"No connected relays"** when the box boots before the network is up) lands on the **"ATM Unavailable"** screen and stays there forever. Providing internet afterward does nothing: - App init is **one-shot** (`App.vue` onMounted try/catch → sets `initError`, never retries). - `initError` only self-clears for `awaiting-fees`; connectivity errors are terminal. - The nostr client reconnects with backoff — but **only after a first successful connect**, so it can't rescue a cold boot with no internet. - The watchdog can't help (renderer is alive and ponging), and systemd `Restart=always` can't help (the process doesn't exit). ## Fix — layered, cheapest-first recovery (see ADR-002 amendment in this PR) 1. **App auto-recovery (first-line, no human).** When the maintenance screen is a *connectivity* fault, the app watches for the browser `online` event (recovers **immediately** when the network returns) plus a 45s backoff safety net (covers online-but-relay-unreachable). Recovery = a **renderer reload** → re-runs init from a clean JS context (no leaked actors/subscriptions), **preserving HAL** in the main process and pairing/`/var/lib` state. 2. **On-screen Retry** — a kiosk-sized button on "ATM Unavailable" for an operator standing at the machine. 3. **SSH/NetBird** stays as the last-resort remote plane — **ADR-002 amended** to state it is *not* the only/first-line recovery. Scoped to connectivity errors only; does **not** hijack `unpaired` (pairing wizard), `awaiting-fees` (self-clears on the operator fee event), or `maintenance` (operator-set). ## Changes - `main.ts`: new `app:recover` IPC → `reloadRenderer()` (resets `secretsConsumed`); **`hal:init` made idempotent** (reuse existing instance) so the recovery reload *and* the pre-existing watchdog crash-reload can't double-open the validator/dispenser serial ports — a latent-bug fix for the watchdog path too. - `preload.ts` / `electron.d.ts`: expose `recoverApp()`. - `App.vue`: recovery scheduler (`online` + backoff) + kiosk Retry button. - `docs/adr/002-*`: amendment — app recovery first-line, SSH/NetBird last-resort. - `flake.nix`: expose `nixosConfigurations.batm3-usb` as a named config (extracted from the inline `disk-image-batm3-usb`) so a running stick's app can be updated in place via `nix copy` + `switch-to-configuration` — no reflash. Disk image builds from the same config. ## Verification - `vue-tsc` typecheck clean. - Deployed in-place to the batm3 (`switch-to-configuration test`): service active, `app:recover` present in the bundle, boots to `idle`, `Fully initialized`. - **Runtime recovery still to be exercised on hardware:** boot/block the relay → confirm "ATM Unavailable" + Retry → restore network → confirm it self-heals. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The access/recovery plane (SSH/NetBird) stands and may carry recovery
procedures, but it is explicitly NOT the only or first-line recovery. Add
a layered, cheapest-first recovery model: (1) app auto-recovery of its own
relay/Lightning connectivity, (2) an on-screen Retry for an operator at the
kiosk, (3) SSH/NetBird as the last-resort remote plane for genuine app/OS
failure. A public kiosk must not need remote shell access to recover from a
transient/boot-before-network outage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A connectivity-type init failure (e.g. "No connected relays" when the box
boots before the network) landed on "ATM Unavailable" permanently: init is
one-shot and the nostr reconnect only helps after a first successful
connect, so a machine never self-healed when internet returned.

Recover by reloading the renderer, which re-runs init from a clean JS
context (no leaked actors/subscriptions) while the main process keeps HAL:
- main.ts: new `app:recover` IPC → reloadRenderer() (resets secretsConsumed).
- hal:init is now idempotent (reuse the existing instance) so the reload —
  and the pre-existing watchdog crash-reload — can't double-open serial ports.
- App.vue: when initError is a connectivity type (not the operator/
  self-clearing states unpaired/awaiting-fees/maintenance), watch for the
  `online` event (recover immediately) plus a 45s backoff safety net, and
  render a kiosk-sized Retry button for a person at the machine.

Preserves pairing + /var/lib state (renderer reload, not a process restart).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lift the USB-variant config (distinct fs labels, nofail /boot, no
growPartition, autoUpgrade off) out of the inline disk-image-batm3-usb
`let` into `nixosConfigurations.batm3-usb`, and build the disk-image from
that same config. Enables in-place app deploys to a running stick via
`nix copy` + `switch-to-configuration` (build the toplevel, copy the
closure, activate) — no reflash, preserving pairing + /var/lib state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
padreug deleted branch feat/connection-recovery 2026-08-05 02:33:02 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/bitspire!82
No description provided.