The daemon config was built by format!-ing a YAML string literal. Upstream's
config structs are #[serde(deny_unknown_fields)], so a key we get wrong does
not degrade — the daemon refuses to start and the node drops off the mesh.
String-built config made that a runtime discovery on a live node.
Replace it with a typed struct tree serialised via serde_yaml, verified
field-by-field against jmcorgan/fips v0.4.1, plus an exact-output snapshot
test so schema drift fails in CI rather than at boot. Also adds tests for
determinism (server.rs compares the render against disk to detect drift, so
instability would cause a reinstall+restart loop) and for the mDNS key path.
Enables node.discovery.lan.enabled (mDNS/DNS-SD, added upstream in v0.4.0)
so co-located nodes peer directly instead of depending on the public anchor
being reachable — an anchor blackhole on one segment currently islands a node
completely. Emitted unconditionally rather than version-gated: v0.3.0's
DiscoveryConfig has no `lan` field and no deny_unknown_fields, so a v0.3.0
daemon ignores it harmlessly and it activates on upgrade with no second
config migration.
Note: the first startup after this lands renders a config that differs from
disk, so the existing drift check reinstalls it and restarts the daemon once.
That is the intended self-healing path and settles immediately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RPC failures logged only the top-level anyhow context, so a peer-files
failure produced exactly "RPC error on content.browse-peer: Failed to
connect to peer" with the real cause discarded. That made it impossible
to tell a dead peer from a slow Tor circuit from a FIPS resolve failure
without reproducing by hand.
Switch the log line to `{:#}` so the whole context chain is rendered.
The client-facing message still uses `{}` through sanitize_error_message,
so no internal detail is leaked to callers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The peer-files header title block is `hidden md:block` (the global header
carries the peer name on mobile), which also hid the transport pill nested
inside it — so mobile users browsing a peer's files had no indication of
whether they were on FIPS or Tor.
Render a `md:hidden` pill alongside the peer icon so the transport is
visible at every width, without duplicating the peer name on desktop.
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
vpn.peer-config returned the config exactly as stored at creation time, so
after a node moves networks the reused companion peer's QR encodes the old
location's address (seen on .116: Endpoint = 10.125.9.0 from the previous
LAN) and the tunnel can never connect. Rewrite the Endpoint line with the
node's current address before rendering the QR, and persist it back so the
downloadable .conf matches. Endpoint detection is shared with
vpn.create-peer via current_wg_endpoint_host().
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>
After an unclean shutdown, crash recovery walked the running-containers
snapshot and retried 'no such container' failures (2 attempts, 10s
backoff) for containers that no longer exist. Recovery runs before the
server binds :5678 and notifies systemd ready, so a stale snapshot
pushed startup past TimeoutStartSec=5min — systemd killed the daemon
mid-recovery, the next boot saw a crash again, and the node looped
forever (151 restarts on .116 after a hard poweroff).
- pre-filter the snapshot against 'podman ps -a' and skip vanished
containers outright (fail-open if the query fails)
- never retry a 'no such container' failure
- extend the systemd start timeout ahead of each container start so a
heavy node recovering dozens of real containers isn't killed while
making progress
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backend: tor.list-services now hides services whose name maps to a
known-but-uninstalled catalog app (aliases covered: bitcoin/knots/core,
electrumx/electrs, btcpay, mempool). The node's own service, the relay,
and custom user services always show.
ISO: first-boot tor setup no longer pre-creates hidden services for six
apps that may never be installed — only the node's own onion is baked.
It also only seeds services.json/torrc on FIRST boot instead of
clobbering backend-managed services on every boot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v1.7.104 shipped ISOs whose units crash-looped (nostr-relay 3s loop,
archipelago-diag 203/EXEC) because build-time extraction failures were mere
warnings and the cached rootfs never tracked its recipe:
- hash the rootfs-defining region of the build script and rebuild when it
changes (stale cache shipped ISOs without wpasupplicant/iw/rfkill)
- refuse to build without nvpn / nostr-rs-relay unless
ALLOW_MISSING_VPN_BINARIES=1
- ConditionPathExists on nostr-relay/nostr-vpn/diag units so a stripped
image skips them instead of crash-looping
- post-install test: every enabled archipelago*/nostr* unit must have an
existing ExecStart payload
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sweep podman-execs into every running container; doing that on each 30s
reconcile tick was a permanent conmon 'Failed to create container' storm on
hosts where exec from the backend's cgroup context fails (Debian 13 first
boot). Ownership drift is an install/OTA-time event — sweep on first pass
after a container appears, then at most hourly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With :global(html.kiosk-mode) .bg-layer the SFC compiler drops the
descendant part and emits bare html.kiosk-mode rules — including
display: none !important — blanking the whole document on kiosk.
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>
neode_intro_seen / neode_onboarding_complete are per-origin browser state:
after a reinstall (or another node on a DHCP-recycled IP) they describe the
previous node and muted a genuinely fresh install's intro. Root boots now
always ask the backend; a confirmed-fresh answer plays the intro and clears
the stale flag.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A stray `archipelago --version` used to start a full second instance next
to the systemd one (issue #74). Handle -V/--version, -h/--help, and reject
unknown dashed options before any tracing/state init, mirroring the
ceremony subcommand's clean-stdout precedent. Version output matches the
health RPC format: <pkg-version>-<git-hash|dev>.
Fixes#74
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both tests take the global single-flight UPDATE_OP_LOCK; run concurrently
by the test harness, one saw the other's lock and failed with 'another
update operation is already running' instead of its expected refusal.
A shared test mutex makes them mutually exclusive deterministically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pre-UI sync overlay used a hardcoded orange box-glyph SVG; it now renders
the same resolved app icon the loading screen uses (with the shared image-error
fallback), so the wait page reads as ElectrumX at a glance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The manifest baked bitcoin-knots:8332 into DAEMON_URL. Nodes whose
backend runs under the bitcoin-core name (and any future variant
without a knots DNS alias) left electrumx permanently disconnected —
'connection problem' forever and a block index stuck at 0. The
startup script now picks the first backend name that resolves on
archy-net and falls back to bitcoin-knots.
Catalog regenerated (catalog manifests override disk ones fleet-wide);
this regen also embeds the recently-merged barkd/Ark manifest for the
first time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
.198 crash-looped 236x on a truncated OTA binary with a good backup
sitting in update-backup/ — verify_pending_update() can't help when
the new binary never runs. New scripts/ota-crash-guard.sh runs as
root from ExecStartPre: while the post-OTA pending-verify marker
exists it counts start attempts, and after 5 failures restores the
backup binary (atomic rename), replaces the marker with an
update-rolled-back.json tombstone, and lets the service come back
on the previous version.
Wiring: fresh ISOs get the ExecStartPre line in the unit file;
existing nodes get a drop-in installed by apply_update's runtime
component step on their next OTA. '+-' prefix so a missing or
failing guard can never block the service.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of .198 bricking on the 1.7.103 OTA: two concurrent
update.download RPCs shared one staging file, cancel_download wiped
staging mid-flight, a third download began re-filling it, and
apply_update mv'd the 3-second-old 17MB partial of the 49MB binary
into /usr/local/bin -> SEGV boot loop (236 restarts, no rollback).
- Single-flight UPDATE_OP_LOCK across download/apply; concurrent
callers get an explicit 'already running' error.
- apply_update now requires the .download-complete marker AND
re-verifies every staged component (size + SHA-256 + BLAKE3)
against the manifest before touching the system.
- cancel_download only wipes staging when no operation holds the
lock; otherwise it just flags the in-flight loop to bail.
- Fixed the 'file already complete' path in
download_component_resumable, which skipped verification and fell
through to the bogus 'download failed without a captured 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>
Dispatch the Enter keydown two frames after the input event so the login
button is enabled when it arrives. Keeps auto-login working against nodes
running older web builds where controller-nav's Enter-in-input pattern
would otherwise click Replay Intro (see the matching web-ui fix).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The companion's auto-login fills the password and dispatches Enter in the
same tick, before Vue re-enables the disabled submit button. Controller-nav's
'Enter in input clicks the next enabled button' pattern then hit the Replay
Intro button — clearing neode_intro_seen and hard-navigating to /, so every
app connect replayed the intro cinematic in a loop. The auth inputs all have
their own Enter handlers, so they opt out via data-controller-no-submit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3aebbcbb made goal completion require walking the manual steps (running
apps alone no longer finish a goal) but left the store tests asserting
the old auto-complete behavior. Tests now walk the manual step and also
pin the new running-but-not-walked => in-progress case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bundling fmcd for offline installs (6dcdada3) shipped the raw
uncompressed podman save tar and grew the ISO ~220MB (2.3G -> 2.5G in
RC6-RC9). Save the core bundle as .tar.zst instead — podman load
auto-detects compression, verified locally against the RC9 fmcd.tar
(228M -> 65M, loads cleanly into rootless storage). The first-boot
loader and installer copy globs now pick up .tar.zst too.
Also trims the installer's USB->disk image copy and first-boot load
time on fresh installs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chromium's SingletonLock is a symlink encoding <hostname>-<pid>. After a
node rename changes the OS hostname, the lock left from the previous
boot reads as "another computer" holding the profile; Chromium refuses
to start, --noerrdialogs hides the only dialog, and the kiosk
black-screens forever.
Two layers:
- kiosk launcher removes Singleton{Lock,Cookie,Socket} before every
Chromium start (any prior owner is dead at that point — pkill at
session start / loop respawn). Ships via ISO splice AND the binary's
include_str! self-heal, so existing nodes get it with the next OTA.
- the rename handler clears the lock immediately as a hostname
side-effect, covering nodes still running a pre-fix launcher.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
auth.setup only wrote the web password hash to user.json, so the
archipelago system user kept the image default password ("archipelago")
on console/SSH even after the user picked a real password during
onboarding. Reuse the existing usermod-based sync (already used by
auth.changePassword's alsoChangeSsh path) at setup time, best-effort so
a failure can never break onboarding.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The card-action sweep put the mobile 50/50 Delete|Rotate row under every
Tor service on desktop too — desktop reverts to the original compact
inline Rotate / trash / toggle beside each row, while mobile keeps the
thumb-safe stacked layout. The VPN and Network Interfaces cards' bottom
buttons now pin to the card bottom (mt-auto) so they stay vertically
aligned when the grid stretches one card taller than the other.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- "Back to Goals" now returns to Home's Setup tab (?tab=setup) instead of
landing on the dashboard tab.
- The channels screen remembers when a setup wizard sent you there
(?from=goal) — its back button reads "Back to Setup" and returns to the
wizard; it also now uses the shared BackButton pill instead of a bare
link.
- "Open & Configure" steps launch the actual app via the app launcher —
iframe apps overlay on top of the wizard, tab-only apps (BTCPay,
Nextcloud) open a tab, mobile uses the in-app browser — instead of
routing to the app-details page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retire the >=1800px compact-header variant globally (Monitoring Details,
Federation Find Nodes/Fleet, Identities Create, VPN Add Device, Network
Interfaces Scan WiFi, Tor Restart/Add Service). Tor service rows get a
50/50 Delete|Rotate row underneath — Delete on the left, away from the
enable toggle, so no missclicks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dashboard tab (default): stat tiles per earning source, 7-day earnings
LineChart (one line per source, daily buckets — earnings are sparse
events so finer buckets would draw noise), active-session count and
revenue-by-service bars from streaming.list-sessions. Configure tab
keeps the old settings screen with the intro copy boxed in a card, plus
a new TollGate WiFi entry (price per minute via openwrt.provision-
tollgate, with a set-up-gateway hint when no router is paired). Demo
mocks gain profit history entries and streaming sessions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Research report on the most frictionless "sign in with your Nostr signer"
flow for node login (NIP-07 extension + NIP-46 QR scan with Amber, password
always kept as fallback), and a plan for adding existing Nostr identities
(nsec import / npub watch-only / browser extension) to the Nostr Identities
screen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>