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>