The Channels panel's 'Funding tx in Mempool' link still hardcoded the
local app (missed in the explorer rollout): now it routes like every
other tx link — local Mempool when running, else the saved external
explorer, with the first-time consent modal setting it up and then
opening the tx. The consent modal moves to z-3600 so it renders above
the Wallet Settings modal that can trigger it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Outside-close listeners move from click to pointerdown: pointerdown
fires before the open-toggle re-render, so the kiosk's slow software
compositing can no longer detach the click target mid-flight and close
the menu the instant it opens. color-scheme:dark (CSS + meta) pins every
native control — select popups, scrollbars, pickers — dark regardless of
the user's OS theme; explicit select/option colors cover Firefox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BaseModal becomes a flex column: title row and the new #header slot
(tabs) stay fixed at the top, #footer (action buttons) fixed at the
bottom, only the default slot scrolls (default max-h 90vh unless the
caller sets one). Wallet Settings migrates: tabs pinned, four duplicated
in-pane Close rows collapse into one pinned footer that carries the
active tab's primary action.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The QR mirrored the browser origin — an operator browsing over Tailscale
handed the phone a 100.x address it has no route to, so pairing silently
never connected (reported 2026-07-22; the 'random password' alongside it
was the device token working as designed). system.get-hostname now also
returns the default-route LAN IPv4; the QR substitutes it (or the mDNS
name) whenever the origin is localhost or a CGNAT/tailnet 100.64/10 IP.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AppLauncherOverlay sat at z-2400 under BaseModal's z-3000, so an app
opened from e.g. the Transactions modal loaded invisibly underneath it.
z-4000 lets the existing enter animation play over the modal; closing
the app returns to the modal you came from.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pruned nodes can't run the Mempool app, but tx links blindly opened it
anyway. Now: local app when running; otherwise an external explorer
(default tx1138.com) behind a one-time amber consent modal that spells
out what the other server's operator learns (tx of interest + IP) and
lets the user point at their own instance (placeholder mempool.guide).
Wallet Settings gains an On-chain tab (explorer URL + don't-warn toggle);
tabs renamed Cashu/Fedi so five fit in the row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LND's RPC answers before its p2p server finishes loading, so connect/open
during that window failed red with the raw "server is still in the
process of starting". Now: quiet ~30s retry first; if still starting, a
calm amber "still finishing its startup" notice instead of an error.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Android/rust/archy-fips-core: leaf-only fips node as a JNI cdylib (fips
pinned to the fips-native fork rev with VpnService fd support), built by
gradle via cargo-ndk (arm64), tested on host
- ArchyVpnService: split-tunnel VpnService routing only fd00::/8 (MTU 1280,
foreground specialUse); FipsManager handles the one-time VPN consent and
silent auto-start — no settings surface at all
- pairing QR now fully configures the mesh: fnpub/fip/fhost/fudp/ftcp plus
fanchors (the node's seed-anchor list, npub@addr/transport) so the phone
can rendezvous through public anchors when the LAN endpoint is unreachable
- default seed anchors gain the two dual-transport join.fips.network test
anchors (23.182.128.74:443/tcp, 217.77.8.91:443/tcp); anchor adverts are
Nostr kind-37195 events
- device token rides the password field end-to-end: backend accepts tokens
wherever it accepts the password, so scan = instant login (WebSocket auth
+ WebView form injection unchanged); token logins skip TOTP
- ServerEntry.meshIp + IPv6-bracketed URLs; WebView retries the mesh address
on main-frame errors, auto-login and origin checks honor it
- companion v0.5.0 (versionCode 20), arm64 abiFilter; FipsNative.available
gates everything so non-arm64 still runs as a plain companion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plugging in any LoRa radio now surfaces the device setup modal EVERY time
(dismissals clear on unplug; works while mesh is enabled; 2-poll debounce
so ordinary reconnect blips don't flash it), showing what's currently on
the stick before anything is written:
- mesh.probe-device RPC: identifies the firmware read-only in the same
Reticulum->Meshcore->Meshtastic order as auto-detect. Meshtastic yields
region/modem-preset/channels (want_config is a read); MeshCore yields
name/node-id + firmware version via the previously-unused
CMD_DEVICE_QUERY; RNode via the bare KISS DETECT (no daemon spawn).
Path must be a detected candidate port; the live session's port refuses.
- "Keep As Is": manage_radio=false persists in MeshConfig and the session
skips every on-connect config write (region, channel, PHY params, advert
name) — the radio runs exactly as flashed, hot-swappable.
- "Set Up with Archipelago Settings": second screen shows the params that
will be applied (channel, region, the node's persisted RF params - e.g.
the validated Portugal preset - with the per-region community plan as
fallback) before writing anything.
- Stale-type fix: disconnect now resets device_type/firmware_version, so
a swapped stick no longer shows the previous radio's firmware; probed
kind pins device_kind so the right probe runs first on connect.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Wallet scan modal: fallback buttons no longer flash on open (spinner while
the camera auto-starts, video hidden until live — also kills the Android
WebView play-glyph), decode rate 10/s -> 4/s (preview visibly lagged on
phones), clear messages for BOLT12 offers and LNURL/lightning addresses
(unsupported by the LND backend; full LNURL-pay is a queued feature),
zero-amount invoices in BIP21 URIs prefill from amount=.
- Pine 1.2.0: launch-page copy reflects the auto-seeded HA wiring (only
speaker pairing stays manual), adds the ask-Archy hint and the
silent-answers power-cycle troubleshooting tip (firmware FIFO bug).
- bitcoin-core/knots: rpcuser/rpcpassword move from bitcoind argv (world-
readable in host ps) to a 0600 config file inside the container tmpfs;
the salted txrelay rpcauth hash stays on argv. Catalog regenerated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The kiosk image ships no color-emoji font, so the wallet card's 🥜 and ⚓
drew as empty squares on TVs. Swap them for inline stroke SVGs matching
the neighbouring rows (wallet card + scan modal), and add
fonts-noto-color-emoji to the ISO package set so remaining emoji across
the UI (peer files, chat, content) render on future installs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two gaps kept the wallet QR scanner camera-less outside a desktop browser:
- Companion app: the WebView's default WebChromeClient silently denies
getUserMedia. Both WebViews (main + in-app overlay) now implement
onPermissionRequest — granting video capture only, requesting the
app-level CAMERA permission on first use (manifest already declares it).
- Plain-http origins (mobile web/PWA on a LAN node): browsers hide
navigator.mediaDevices entirely, no permission can bring it back. New
"Take photo of QR" fallback uses <input capture=environment> — the
native camera needs no secure context — and decodes the shot locally
with qr-scanner's scanImage. Live preview still used when available.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All Zeus channel surfaces now read: "Open a channel with Zeus Olympus
node and start sending and receiving Lightning payments. Minimum
150,000 · maximum 1,500,000 on-chain sats required."
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New WalletScanModal scans QR codes with the device camera (BarcodeDetector
with jsQR fallback) including animated/multi-frame QRs via qrloop, then
routes what it saw: BOLT11 invoices -> lightning pay (amount locked from
the invoice when present), bitcoin:/BIP21 URIs -> on-chain send, Cashu
tokens -> redeem, Fedimint invites -> join. Entry points: camera button on
the wallet card (far right), and a Scan button centered between the
Close/Send and Close/Receive buttons of both modals.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vue-tsc in the release build (unlike the gate's type-check) rejects
indexing WG_RETRY_DELAYS_MS with a bare counter; hoist the delay into a
local and gate on undefined instead of the length check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On a fresh node the wgqr step is often reached while the backend is still
settling (services starting, backend restarting during orchestration): a
single 'Failed to fetch' left a permanently blank QR. Retry network-class
failures on a 2s/4s/8s ladder while the step is still on screen, then fall
back to the friendly error + Try again button.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Never show the get-the-app pitch inside the companion WebView itself
- Don't guess peer-exists when list-peers is unreachable: try create,
fall back to peer-config on already-exists errors
- Translate raw 'Failed to fetch' into an actionable network hint and
add a Try again button instead of a dead-end error
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SW-update modal is noise on the demo (nothing to update to) and both
accept-paths end in a reload that replays the intro from scratch. With
skipWaiting/clientsClaim off, ignoring the waiting worker is safe — the
new build activates on the visitor's next session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every Lightning setup (Open a Shop, Accept Payments, Run a Lightning Node)
gains a guided path to a working channel:
- New "Fund Your Bitcoin Wallet" step, gated on the blockchain finishing
its initial sync: while syncing it shows a live progress bar with a
counting-down time-remaining estimate; once synced it shows the on-chain
balance and a "Fund Wallet" button that opens the receive modal with a
fresh address, QR, and the Zeus channel limits (min 150,000 / max
1,500,000 sats) noted.
- When the sync finishes while a Lightning setup is mid-flight, a toast
pops with a "Finish setup" link straight back to the wizard (toasts now
support action links). If several setups are in flight, one is chosen —
the shared steps complete the lightning part of any of them.
- Channel steps are Zeus-branded (logo + copy) and land on the Lightning
Channels screen, which now carries an "Open a channel with Zeus" card
that prefills the open-channel modal with the Olympus peer URI, 150k
sats, and private-channel checked. "Get Zeus" links to zeusln.com.
- Setup completion now actually requires walking the manual steps (fund,
open channel, configure) — previously any step whose app was installed
was silently auto-ticked and running apps marked the whole goal done.
- Completion CTAs now go to the app you just set up ("Go to my shop
(BTCPay)" etc.) instead of the generic services list; iframe apps open
in the on-top app overlay.
- On-chain send modal gains a "Send all funds" sweep toggle, backed by
LND's send_all on the backend (amount no longer required when sweeping).
- Demo/mock: bitcoin.getinfo now returns the block_height/sync_progress
contract the UI reads and simulates a ~90s IBD ramp per visitor so the
timer, toast, and fund flow can all be demoed live; channel list data
fixed (status/channel_point/liquidity totals — the panel previously
crashed on the missing status field); sendcoins supports send_all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After 'I've installed it' the modal now walks WireGuard setup before
pairing: install WireGuard (hosted APK + QR), scan the node's tunnel
config (auto-provisioned 'companion-phone' peer via the existing
vpn.create-peer/peer-config RPCs, rendered client-side), switch it on,
then pair. Real nodes pair against http://10.44.0.1 — the tunnel only
routes AllowedIPs 10.44.0.0/16, so the LAN origin is unreachable
remotely. The demo fakes the tunnel via mock RPCs and keeps its public
pairing URL. REMOTE_ACCESS_STEPS=false restores the old two-screen flow.
Phones get the config as a downloadable .conf (can't scan own screen).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Camera decode failed because every lever was at its minimum: the web
modal generated the QR at 112px (~2.5px per module before upscaling),
the analysis stream was CameraX's 640x480 default, and ZXing ran
without TRY_HARDER. Generate the QR at 512px (displayed 192px with a
real quiet zone), analyze at 1280x720, decode with TRY_HARDER plus an
inverted-luminance fallback. v0.4.14 (versionCode 18).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The App Store gains a "Your Node. In Your Pocket." hero banner for the
mobile companion app, in the same format as the featured-app banner, with
the app running on a phone mockup rising out of the right edge. Install
(or clicking anywhere on it) opens the Remote Companion download/pairing
modal — now openable on demand via a shared trigger instead of only
auto-showing once.
Also fixes the companion APK download on PWA installs: /packages/ now
bypasses the service worker's SPA fallback, which was answering the
download click with index.html instead of the file.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Companion side of docs/companion-pairing-qr.md (v1 contract):
- Connect screen lands on Scan Node's QR / Enter Manually; NESMenu gains
an add-server-by-QR icon button
- QrScannerOverlay: CameraX + ZXing core (pinned, on-device, no ML Kit)
- ServerQrParser: archipelago://pair?v=1&url=..[&pw=..]; unknown major v
-> 'update the app'; upsert by origin so re-scans never duplicate
- archipelago:// deep link registered (singleTask + onNewIntent) for the
modal's 'Open in companion app' button
- WebView auto-fills and submits Login.vue's password form when a stored
password exists — demo QR is one step to a logged-in session
- Web modal + handoff doc copy synced to the real label: Scan Node's QR
- v0.4.13 (versionCode 17)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Remote Companion intro modal gains a second screen: an "I've installed
it" button next to the download button slides over to a pairing QR the
companion app scans to load the server details automatically, with a Back
button to return. The QR is an archipelago://pair deep link carrying the
server URL — the demo embeds https://demo.archipelago-foundation.org plus
the shared demo password so a scan lands in a logged-in demo session; real
nodes send only the address (kiosk advertises the mDNS .local name since
its own origin is localhost). Phones also get a tap-to-open deep-link
button since you can't scan your own screen.
App-side contract + requirements handed off in docs/companion-pairing-qr.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two "unpolished" moments, worst on the demo:
- CompanionIntroOverlay gated on a point-in-time check of the reveal
cinematic flag. On a cold cache the entrance video can start buffering
after the 5s base delay, so the flag was still false when sampled and
the popup cut into the cinematic anyway. It now shows only after the
scene has been continuously calm for the full grace window.
- ConnectionBanner's flat 2.5s debounce fired on every tab-return and on
first dashboard paint: a dead WebSocket is the NORMAL state right then
(browsers kill background-tab sockets; first paint races the initial
connect), and reconnects routinely exceed 2.5s over real links. Those
moments now get a 10s runway (15s window after load/resume); genuine
mid-session drops keep the 2.5s response. On the demo the blip banner
is suppressed entirely — it runs against a local mock, so "Connection
lost" there is pure noise.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A genuine service-worker update still reloaded the page the instant the
new worker claimed it. The kiosk and the public demo replay the intro on
every boot/visit, so an update landing mid-sequence restarted the whole
cinematic — the "typing intro resets to the start after several seconds"
report. Hold the reload until the splash is complete and the
dashboard-reveal cinematic is over, then apply it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clicking 'Back up now' on the Lightning seed banner only navigated to
the LND app page — and the backup card there hid itself permanently if
its single status fetch failed (common right after a fresh install),
so the click appeared to do nothing.
The banner now deep-links with ?seed-backup=1, which the LND page uses
to open the reveal flow directly, and the card retries its status fetch
with backoff instead of giving up after one failed RPC.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two causes of the fresh-install 'loads, refreshes, loads again' jank:
- The PWA autoUpdate service worker claims the page right after the very
first load, firing controllerchange, which unconditionally reloaded the
page. Now only a *replaced* controller (a real update) triggers reload.
- A single 2s RPC echo decided server-down on first boot, flashing the
BootScreen and then hard-reloading seconds later. A second, 6s attempt
now runs before concluding the backend is down.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The remote-companion popup fired on a blind 5s timer while the
first-visit dashboard entrance cinematic runs for 8s, so on a fresh
install it appeared mid-reveal and broke the intro. The cinematic now
publishes an introCinematicPlaying flag via the loginTransition store,
and the popup waits for it to clear (plus a 2s grace) before showing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- SplashScreen no longer re-checks neode_intro_seen itself: it silently
skipped the whole logo-tap/typing sequence for any browser that had
seen the intro before, even when App.vue explicitly requested it (demo
fresh visits, Replay Intro). App.vue is the single decision point.
- Intro video re-encoded 12.7MB/12.6Mbps → 4.3MB/4.2Mbps (1920x1080 CRF23
faststart, muted track dropped) — visually identical on this
illustrated content; cache-buster bumped to v=8.
- Video is <link rel=preload>ed the moment the splash is scheduled (the
<video> element only mounts ~20s in, so cold caches used to start the
download mid-sequence and stutter).
- nginx demo cache regex now covers mp4/webm/mp3/webp/woff2 so repeat
visits play from browser cache.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FileCard audio clicks now emit play (global GlobalAudioPlayer bar) —
wired through the FileGrid list view, the Cloud My Files list and own
search results. Peer Files already used the bottom bar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Top-level Cloud tabs get their own orange-tinted switcher (clearly
distinct from the category pills); full-width thirds on mobile
- 'All Files' tab renamed Folders; categories only show on the file-list
tabs (My Files / Peer Files / search)
- My Files is now a flat list of every own file rendered with FileCard —
real actions (share/download/delete) and clicking opens the FILE
(media lightbox incl. audio, downloads for documents), never a folder;
own search results get the same treatment
- Folder list rows get card backgrounds (readable over the wallpaper),
same info as before
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Send modal gains an Ark tab (address/invoice/lightning-address via
wallet.ark-send), Receive gains an Ark address + QR tab, and the peer
file purchase confirm offers Ark alongside Cashu/Fedimint. Demo mock
exposes ark_sats in wallet.ecash-balance and deducts the chosen rail on
paid downloads.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ark tab in Wallet Settings (status, balances, receive address,
board/offboard, server config via wallet.ark-*), teal Ark badge in the
transactions modal, Ark row on the home wallet card (hidden until barkd
reports a balance), official bark icon, and wallet.ark-* mocks so the
public demo renders the tab.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Companion QR overlay: demo builds encode the demo's own origin
(/packages/archipelago-companion.apk ships in the web image) instead of
the vps2 raw URL.
- Dockerfile.web/.backend: build-time scrub replaces every remaining
occurrence of the release-server address with registry.demo.internal and
fails the build if any survives.
- Mock backend update-mirror list, sideload help text, and changelog prose
no longer name the server address.
- Copy demo-images workflow into .gitea/workflows/ — Gitea ignores
.github/workflows when .gitea/workflows exists, so the CI never ran.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Open/Close Channel modals sat at z-[60], BEHIND the wallet settings
BaseModal (z-3000) — clicking 'Open Channel' looked like nothing
happened while the form changed in the background. Raised to z-3100.
- TransactionsModal gains rail filter chips (All / On-chain / Lightning
/ Ecash with counts): instant ecash micro-payments were burying the
standard transactions.
- Global LND seed-backup banner: once the wallet exists and the seed is
un-acknowledged, a dismissible glass banner (24h snooze) prompts and
routes to the LND backup card; polls only when authenticated, stops
permanently once acked.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Request-to-Peer (Federation Discover modal + Web5 Node Visibility list)
no longer fires instantly: a BaseModal confirm shows the target, an
optional 280-char message (handshake.connect already carried it), and
Send/Cancel with the standard orange CTA.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plugging in a LoRa radio now pops a global two-step setup modal on any
page: step 1 shows the ACTUAL detected board (25 board SVGs vendored
from the Meshtastic web-flasher, GPL-3.0; matched by USB product string
on native-USB boards, vid:pid heuristics for bridge chips, animated
fallback otherwise); step 2 configures with Archipelago presets — the
LoRa region pre-selected from the node's location (new lat/lon→region
mapping), channel 'archipelago', node name, and an advanced firmware
pin. Options adapt per protocol: region+channel program Meshtastic;
MeshCore shows its community frequency plan (firmware owns RF); RNode
defers to the Reticulum daemon config.
Backend: mesh.configure now accepts lora_region (validated against the
driver's region table) and device_kind (probe pin); mesh.status returns
the persisted values plus per-port USB identity (sysfs vid/pid/product)
for board identification. The Mesh Device tab gains a full settings
form (region with duty-cycle/legality hints, firmware pin, channel,
name, identity broadcast) replacing the read-only panel; the old
Mesh-page-only onboarding modal is superseded by the global flow.
Mock backend: stateful mesh config so the dev UI demos the full
detect→configure round-trip (disable mesh → modal fires with Heltec V3).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Federation List/Map tabs now use the Home mode-switcher tablist
(full-width on mobile, shrink-wrapped on desktop, proper tablist a11y)
- Network Interfaces WLAN adapter toggle replaced with the shared
ToggleSwitch used by AI data access in Settings; ToggleSwitch gains
optional disabled/ariaLabel props (non-breaking)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The app-details credentials/version sidebar and the container-app Launch
button used solid bg-blue-600 CTAs and blue links/focus rings instead of
the global glass style; InstallVersionModal used an off-token solid
orange. All primary CTAs now use glass-button glass-button-warning (the
Create Identity orange) and links/focus accents use the orange tokens.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Launching an app (or opening a tx in mempool from Home/wallet/channels) used
to router.push the app-session page in overlay/fullscreen modes, swapping the
page underneath — confusing and lossy. All display modes are now store-driven:
panel renders beside the page, overlay/fullscreen render above it (teleported
to body), and the route never changes. Deep-link paths (/tx/<hash>) ride along
via the launcher's panelPath instead of a route query; closing always returns
exactly where the user was. The app-session route stays for direct links.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>