setup-node-ca.sh writes one 'listen <addr>:443 ssl;' per LAN address at
the moment it runs (per-address on purpose — Tailscale holds :443 on the
tailnet address) and its idempotency guard never revisits them. nginx
REFUSES TO START while any listen address is missing, so this takes the
entire dashboard down, not just HTTPS:
1. the node moves networks and the old address is gone; or
2. nginx starts before DHCP assigns the address — and nginx.service
ships no Restart=, making that single race permanent.
Both hit archi-dev-box today: nginx dead since boot on 'bind() to
192.168.63.240:443 failed (99: Cannot assign requested address)', the
dashboard simply unreachable, which is exactly the symptom a user with
no screen cannot diagnose.
run_nginx_listener_repair drops listeners for absent addresses, adds one
per present address (CGNAT excluded), installs behind with
rollback, then starts nginx if it is down and gives it a
Restart=on-failure drop-in so the boot race stops being fatal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A codebase sweep for siblings of e282c059 (blocking network I/O parked
on the tokio runtime) found the openwrt fix was incomplete:
- openwrt.scan: scan_subnet is async in name only — up to 255 SEQUENTIAL
blocking TCP probes at 500ms each (~2 min on a /24 that silently
drops) plus a blocking SSH verify per candidate. One click of 'scan
for routers' held a worker for that whole time. Now spawn_blocking.
- provision-tollgate / scan-wifi / configure-wan still ran their SSH
exchanges inline; bounded_tcp caps each socket op but a session is
many sequential ops (provision runs opkg install over SSH), so worst
case was minutes. All three now spawn_blocking.
- network::check_dns: blocking glibc to_socket_addrs with no app-level
bound, on every Server-tab load via network.diagnostics. Against a
stale resolver — the moved-network case — that is 5-40s per refresh.
Now spawn_blocking plus a 5s cap, so the tile reports 'no DNS'
instead of hanging.
Verified false positives left alone: every other bare TcpStream::connect
targets 127.0.0.1 (fails instantly), and every remote reqwest client
already sets a timeout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
check_upnp_available uses a blocking std UdpSocket and, on a network
with no UPnP gateway (the normal case right after a node moves), runs
out its full 3s read timeout. Inline on the runtime that blocked a
worker on every call, from four call sites. Same class as the OpenWrt
SSH stall (e282c059), smaller blast radius — move it to spawn_blocking.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Router::connect/connect_password did a blocking std TcpStream::connect
with no timeout, inline on the tokio runtime. Against a router that
stayed behind when its node moved networks (framework-pt, 2026-08-15),
every dashboard poll of openwrt.get-status parked a worker thread for
the OS connect timeout (~2 min) — overlapping polls stalled unrelated
RPCs for 25s+ at a time, sessions timed out, and TOTP codes expired
before the backend verified them.
- bounded_tcp(): 5s connect timeout + 30s read/write timeouts on the
session socket, shared by both connect paths.
- openwrt.get-status runs its SSH exchange on spawn_blocking, so even a
slow router can only slow its own tile, never the API.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Small-disk nodes (<1000 GB data volume) keep the same dynamic
prune-vs-archival logic but now retain ~50 GB of recent blocks instead
of the bare 550 MB minimum. Takes effect for catalog-covered installs
at the next catalog regeneration + signing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both failure modes are from framework-pt relocating (2026-08-15):
- archy-ha-btc-rpc-proxy bound socat to the LAN IP baked in at unit
generation; after a move the address no longer exists and the unit
restart-looped forever (counter 2446). run_ha_rpc_proxy_bind_repair
rewrites ExecStart to compute the bind address at each start, so
Restart=always itself heals any future move.
- homeassistant's quadlet pointed at the domain image ref with --pull
never while local storage held the same name:tag under the bare-IP
registry ref (catalog signing rename) — 761 restarts on 'image not
known'. run_pull_never_image_repair retags a matching local image;
it deliberately never pulls.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The welcome banner picked its address with 'hostname -I | awk {print $1}',
so a node with WireGuard up advertised 10.44.0.1 — its own tunnel address,
present on EVERY node — as its web ui / ssh address. Off-tunnel that is
unreachable, and after a headless box moves to a new network it is exactly
the wrong thing to trust (framework-pt, 2026-08-15).
- Pick the default route's source address; fall back to the first address
that is not WireGuard 10.44/16, CGNAT 100.64/10, or loopback.
- Also print http://<hostname>.local when avahi is up — the one address
that survives any DHCP change, which is the real answer for headless
boxes that move between networks.
- scripts/welcome-banner.sh is the new canonical copy, embedded in the
binary (tor-helper pattern): bootstrap::run_welcome_banner_sync rewrites
/etc/profile.d/archipelago.sh on ISO-installed nodes at startup, so the
fix reaches the deployed fleet with the next OTA instead of only fresh
ISOs. Machines without an installer-baked banner are left untouched.
- Same fix inlined in the live ISO builder's PROFILE heredoc
(image-recipe/_archived/build-auto-installer-iso.sh).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The badge is now the dashboard login's AnimatedLogo, square for square:
inline SVG (20 white rects, 100ms stagger, 3s loop) inside the same
gradient ring. The old <img> of favico-black-v2.svg baked a second ring
into the ring and couldn't animate; the asset leaves the gate allowlist
since nothing references it now.
- The submit button is .glass-button longhand: hover lift + lightening +
rim glow, active press, disabled dim — the flat darken-only hover read
as broken next to /login.
- Loading state: submitting flips the button to spinner + 'Signing in…'/
'Verifying…' and disables it, via a single inline script admitted by
CSP sha256 hash (not unsafe-inline; injected markup stays inert, and
the page still works as a plain POST without JS).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The D-04 Routstr leg was fallback-only — never user-selectable, and its
Nostr discovery parses a docs-shaped event content ({endpoints, models,
pricing}) that live kind-38421 announcements don't actually carry
({name, about}), so it could never match a real provider. This adds the
explicit path AIUI's model picker needs: /aiui/api/routstr/models
passes through the live aggregator catalog (the instance routstr.com's
own frontend queries; the canonical api.routstr.com 404s), and
/aiui/api/routstr/chat/completions makes one paid, non-streaming,
OpenAI-shaped call — session-gated, egress-screened (S3), refused
without an armed operator budget (D-05), paid via auto_pay_token,
change and refused-request tokens redeemed back into the wallet so a
failed attempt nets zero (verified live: quoted=1 reclaimed=1 net=0).
nginx template gains the location in both server blocks (T-13-15).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- lnd.createinvoice now returns r_hash_hex; new lnd.invoicestatus RPC
looks the invoice up (SETTLED + amt_paid_sat). E2E-verified on this
box: real invoice minted, status polls settled:false until paid.
- Receive modal: Lightning polls settlement every 3s and flips to the
on-chain-style success view — straight to the green check + amount
(no broadcast step; settlement is final). Raw bolt11 text removed:
QR + CopyButton only. State fully reset per open/close.
- CopyButton is now the wallet's only copy affordance: the ark-address
and ecash-token holdouts swapped in, their ad-hoc handlers deleted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WebUI RULE (operator, 2026-08-14): never point users at a terminal. The
certificate section told users to run setup-node-ca.sh by hand — it now
has a Generate button backed by system.node-ca.generate, which runs the
idempotent script server-side (live-tested: generated and /ca.crt serves).
Routstr budget panel moves directly under the Claude API key card.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The animated federation map froze the framework-pt 4K TV: the launcher
held every machine to the HD 5500-era choppy-audio flags (single raster
thread, GpuRasterization banned) while the map wrote SVG attrs at 60fps.
- Launcher: two flag tiers. legacy = the proven conservative set; modern
(Intel gen8+, 'NNth Gen' models, AMD Ryzen) = default raster threads +
GPU rasterization. Classified from /proc/cpuinfo (11 model strings
covered by tests in-session); KIOSK_GRAPHICS=performance|quality in
kiosk-display.conf overrides; headless unchanged. Reaches deployed
kiosks via the include_str! self-heal, same as the vsync fix.
- system.kiosk-display.get/set: carries a 'graphics' field alongside
'preset'; setting one no longer clobbers the other.
- Settings → Display: Graphics picker (Auto / Compatibility / Quality).
- NetworkMap3D: kiosks default to the 2D projection (remembered toggle
still works) and tick at half rate with carried-over deltas — same
spin speed, half the paint cost.
- Changelog: curated Unreleased notes for all of the above + the gate
frame-embedding fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Apps that ship X-Frame-Options (Alby Hub: DENY) or a CSP frame-ancestors
directive rendered as a dead grey pane in the dashboard's embedded app
session; the historical fix was a bespoke per-app nginx strip proxy
(gitea). The gate now removes X-Frame-Options and strips ONLY the
frame-ancestors directive from proxied responses — the rest of the app's
CSP passes through untouched. The clickjacking threat those headers
address is handled the same way the gate's own pages handle it: every
proxied request is authenticated first, and the gate already declares
permissive frame-ancestors on its own responses. Unit-tested; verified
live on archi-dev-box (Alby Hub embeds, CSP intact).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>