Cold-start node picker: when more than one node is saved, launching the
app asks which one to connect to instead of silently reusing the last.
The gate is process-scoped (LaunchGate), so rotation and returns from
background never re-ask — only a genuinely cold start does.
Non-FIPS nodes no longer raise our tunnel. ServerEntry.isFipsNode() is
true only for nodes that arrived from a FIPS-capable pairing QR (npub or
mesh ULA). Android permits exactly one VPN at a time, so starting ours
for a hand-entered LAN box would silently steal the tunnel from whatever
the user actually uses to reach it.
QR scanning rebuilt on the ZXing-Java pipeline — flat per-frame cost,
ROI + half-frame coverage, AE fps floor, 1s AF auto-cancel, and the
zoom hunt calmed down (step after 4s, hold >=6s, snap to 1x on decode,
never zoom and refocus on the same tick). The old escalation-on-failure
and one-shot AF lock are gone and should not return; the zxing-cpp
alternative is written up in docs/ and deliberately not actioned.
FipsManager.registerNode/autoStartIfReady now self-dispatch to IO. Both
are reached from Compose scopes, and dlopening the 7 MB mesh core plus
the VpnService.prepare binder hop were freezing the frame right after a
pairing QR decoded — which read as a slow scanner when the scan had
already succeeded.
Roaming: WebViewScreen re-probes its origin on any transport change
(1.5s debounce) and hops LAN <-> mesh, since SPA XHR/WS failures never
fire onReceivedError and left the page on a dead 192.168.x.x origin.
ArchyVpnService coalesces the warmer restart behind a 2s debounce so
marginal Wi-Fi flapping stops triggering a warmer pass per flip. Do NOT
switch that to registerDefaultNetworkCallback — the app routes through
its own TUN, so its default network IS the VPN and the callback would
fire once and never again.
Typography moves to Montserrat (OFL notice included), with a new
SlidingLoader and refreshed mesh loading + splash logo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>