The mobile nav sampled --safe-area-top once at mount, but the companion
WebView injects it asynchronously. An authenticated session mounts the
dashboard before the injection lands (fresh installs mount after login,
long after it), so the content padding baked in 0 while the fixed tab
bar grew by the real inset — content slid underneath by exactly the
status-bar height, the update-install-only overlap.
Re-read on the WebView's new archy-insets event, with a retry ladder as
fallback for APKs that predate the event.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause (diagnosed live via CDP on a Pixel 9a): camera permission
granted and the native camera streamed (2m35s active) but ZXing returned
zero decodes for Lightning-invoice QRs — window.__archyQrResult received
0 calls. Dense BOLT11 codes need more pixels/module AND sharp focus; the
9a's main lens rests at a far focus and 1280x720 wasn't enough, so dense
invoices never resolved while sparse address QRs did. Fix: analysis at
1920x1080 + a repeating centre autofocus tick (a static hand-held QR
never retriggers continuous-AF on its own).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Slow multi-hop payments (>15s routing) surfaced as "Payment failed"
while LND settled them in the background: the shared LND REST client's
15s total timeout aborted the synchronous /v1/channels/transactions
wait, and every UI path treated that abort as a definitive failure. The
payment then succeeded anyway and only appeared in history on the next
background poll.
Backend: lnd.payinvoice now decodes the invoice up front for its payment
hash, pays on a dedicated 120s client, and answers status:"pending" with
the hash (never an error) when the wait elapses after the payment was
handed to LND — only a pre-connect failure is still a hard error. New
lnd.paymentstatus RPC reports succeeded/failed/in_flight (with humanized
failure reasons) from /v1/payments.
Frontend: new rpcClient.payLightningInvoice() pays then polls
lnd.paymentstatus to a real terminal state (3s interval, up to 2 min);
all five call sites (send modal, scan modal, web5 unified send, peer-file
purchase, app-launcher payments) migrated. Failure is only shown when LND
itself declares FAILED; a still-settling payment shows an in-flight state
and success fires the transaction refresh.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bundles all post-115 fixes into an OTA release: boot READY-before-recovery
+ Restart=always (no more 'server starting up'), and the app-install
daemon-kill fix (v6 relay only bridges live app ports; port-cleanup
excludes our own PID).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- app webview draws behind the status bar again (the inset rework
painted an opaque black bar there); page background owns the top
- the in-app overlay eats EVERY touch its children don't handle —
a near-miss on Close was falling through to the kiosk AIUI tab
- loading screen never titles itself with a raw mesh IPv6/IP host
- re-land vc37 connect fixes vc38 shipped without: pairing restarts
the mesh immediately when consent exists; session warmer probes all
targets concurrently (5s) instead of 20s each in sequence
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fips::config::install() now writes /etc/fips/fips.d/80-web-ui.nft
(tcp 80/8443 accept) and reloads the baseline on every install/upgrade —
the hardening firewall default-denies inbound on fips0 and the UI was
unreachable over the mesh without it (root-caused live 2026-07-26).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plain-text seed QRs didn't scan into Passport Prime — wallets that
import seeds by QR (Passport, SeedSigner, Keystone, Nunchuk, Sparrow)
expect the SeedQR standard: each BIP39 word as its zero-padded 4-digit
wordlist index, concatenated into a numeric QR.
- new utils/seedqr.ts encodes BIP39 words per the SeedSigner spec
(@scure/bip39 wordlist; vector-checked abandon=0000, zoo=2047)
- new shared SeedRevealPanel (Words/QR tabs, tap-to-reveal blur) now
backs the LND reveal AND the Settings→Backup recovery-phrase reveal,
so every current and future seed reveal behaves the same
- onboarding seed + Settings reveal: QR defaults to SeedQR with a
plain-text toggle
- LND seed stays plain-text-only with an explicit note: aezeed is not
BIP39 and only restores into LND-based wallets (Zeus/Blixt/another
node) — SeedQR-encoding it would just mislead
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both seed screens get the wallet-settings segmented tab style: words
stay the default first view; the QR tab renders the space-joined seed
words for wallets that support seed import by scan. The LND reveal QR
sits behind the same tap-to-reveal blur as the words, and both panes
warn that the code is equivalent to the words.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- sats/BTC unit toggle on the on-chain amount field with live conversion
hint; canonical value stays sats end-to-end
- Fast / Standard / Slow fee presets (1 / 6 / 144 block targets) plus a
custom pane taking target blocks or an explicit sat/vB rate
- confirm pane shows LND's fee estimate for the chosen speed via the new
lnd.estimatefee RPC (GET /v1/transactions/fee)
- lnd.sendcoins now accepts target_conf / sat_per_vbyte with the same
mutual-exclusion + range validation as channel opens
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consumes the lnd.closedchannels RPC and closing/force_closing statuses
that shipped backend-side in 00b7e179 but never reached the panel:
- tab bar with per-state counts; pending covers pending_open/closing/force_closing
- closed-channel cards with close type, settled balance, block height and
closing-tx explorer link
- closing/force_closing status dots + closing-tx link on in-flight closes
- Close button hidden for channels already mid-close
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wallet card (Home + Web5 views) now leads with a Total Bitcoin row
summing all rails (on-chain + lightning + cashu + fedimint + ark), using
the orange ₿ mark. The on-chain row swaps ₿ for a link/chain icon.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins fips to Zazawowow/fips-native fast-join-pinned (upstream pinned rev
46494a74 + one patch: pending/queued discovery lookups re-fire the
moment the tree parent lands instead of riding out timeouts started
before the mesh could answer). Measured: session lands 225ms after the
route appears; fresh-join route wait no longer stacks doomed lookups.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First lookups fire before the phone's tree position settles and are
doomed; at 10s completion timeout each one cost 10s before the 1s
retry could fire (observed 19s to a route on a fresh 5G join, session
3ms after the route — discovery convergence was the whole cost).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stock fips pacing is tuned for always-on routers: a failed discovery
backs off 30s, session resends gap out to 8-16s, dead links redial at
5s→300s. On a phone that stacked into a 40s+ first connect over
cellular. Config-only tuning in build_config (no fork changes):
discovery backoff 1s (cap 30s), session resends 400ms x1.5 (10 max),
link redial 1s (cap 30s). Measured on-device: anchor +1.2s, node
session +5.4s from cold start over 5G.
Ops note: vps2 anchor daemon had degraded again (multi-second link
RTTs after 15h uptime) — restarted, recycle tightened 1d → 6h.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A pairing QR minted while the node UI was browsed over the mesh carried
npub….fips as fhost. Android's system DNS can't resolve .fips, so every
direct handshake dial failed and first connect crawled through public-
anchor discovery (2m35s observed) instead of one LAN hop.
- node UI: resolveServerUrl treats .fips names and IPv6 ULA literals as
phone-unreachable and substitutes the node's LAN IP (same guard as
tailnet/localhost)
- app: QR parser falls back to the url param's host when fhost is .fips
- app: peer store never keeps .fips dial addresses
- app: peer aliases slugged for the fips host map (spaces dropped
"Framework PT"/"Mesh anchor" from name resolution entirely)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A settled payment now takes over the modal: animated check with radiating
rings (emerald + one Archipelago-orange), the amount large, method label,
then copyable payment hash (lightning) / transaction ID (on-chain) and a
settling note, with Send-another / Done. Replaces the old alert-line at the
bottom of the form. Reduced-motion honored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gesture-nav strip below the in-app browser's control bar was painted
black by inset padding, but painted-only areas don't consume touches in
Compose — stray taps fell straight THROUGH the overlay into the kiosk's
tab bar behind it (accidentally opening the AIUI chat tab). The strip is
now an explicit Box: solid black AND click-swallowing. Bottom inset
moved off the Column padding so the shield can own that region.
Served APK: 0.5.11 (vc31).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Why apps feel instant over the mesh and data panels didn't: an app
launch is ONE streamed HTTP fetch on a warm session; the panels were
built from SERIALIZED RPC round-trips, each costing a full mesh RTT.
- Wallet card (Home): the 7 balance/history RPCs fired one-by-one
(seconds by construction) — now all 7 in parallel, and the card paints
a persisted last-known snapshot (balances + recent transactions)
BEFORE any network round-trip. Refresh replaces it silently.
- Web5 connected nodes: listPeers + federationListNodes fetched
together instead of stacked.
- Peer files: the cosmetic peer-name lookup no longer blocks the
catalog/owned fetches it never depended on.
Rules going forward: never serialize independent RPCs; never show a
spinner where last-known data exists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Apps-tab launches awaited package.credentials with a 5s timeout before
opening anything (home-card launches skip the gate — that's why they
always felt instant). The lookup now races a 1.2s budget: past it, the
app launches with the static fallback config while the RPC finishes
into a per-app memo, making every subsequent launch instant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- SendBitcoinModal crashed AT MOUNT on device ('Cannot access R before
initialization'): the invoice-first block's watch() evaluates its
computed source at setup, which read effectiveMethod while still in
its temporal dead zone (declared 20 lines later). Send button did
nothing and the failure poisoned sibling modal data. Block moved below
effectiveMethod, with a comment so it can't drift back.
- Cold start injected 0px safe-area vars (onPageFinished can beat window
attachment; rootWindowInsets null) — UI lost top/bottom margins and
the tab bar sat in the gesture zone eating taps. Injection extracted
to injectSafeAreaVars(), re-fired from an OnApplyWindowInsetsListener
when real insets arrive, on page finish, and on reattach.
- MeshLoadingScreen wears the proper brand: pixel 'a' in the black
circle-container + F*CK IPS MESH wordmark.
Served APK: 0.5.10 (vc30). Reproduced+diagnosed live on-device.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Everything from tonight's remote field testing:
- Back-to-dashboard fixed: JS bridges on the retained WebView delegated
through live-composition callbacks (stale closures made apps refuse to
launch after remote ⇄ dashboard), and reattach forces a layout pass
(top/bottom UI was wrong until a tap).
- F*CK IPs MESH branded loader: full-screen on relaunch (startup race)
and during the first connect after scanning a node QR.
- Party 'Share this app' is now a QR of the public vps2 download link
(scan with any camera → install over any internet); direct APK
file-share kept as a secondary action.
- Mesh party pairing is MUTUAL: scanner announces itself to the scanned
phone's Flare /hello — both sides get the peer, a chat entry and a
'👋 joined the party' message; scanner auto-opens the chat.
- Party scanner asks for CAMERA permission (fresh installs/reinstalls
landed on a black preview).
- Node: device tokens mint unique companion-<id> names — pairing a
second phone no longer revokes the first phone's login (the source of
'reconnecting a lot' and the second phone's failure).
Served APK: 0.5.9 (vc29).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The WEB_VIEW route recreated the WebView on every return from the
remote screen — full reload + re-login every time. The kiosk WebView is
now retained in a holder and reattached live (no race, no reload);
clients/bridges/listeners are re-bound to the new composition on
reattach, and the instance is dropped on retry, disconnect, or server
change so errored/stale sessions still reload for real.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Party screen 'Share this app': shares this install's own APK through
the system sheet (FileProvider over cache/share/), so a nearby friend
gets the companion via Quick Share/Bluetooth with zero internet — the
party-mode premise end to end.
- Intro screen: Mesh Party button under Get Started — party works with
no node at all, so it belongs on the first screen a fresh install
shows.
- Served APK: 0.5.8 (vc28), ready for the QR/OTA/ISO cut.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Combined session state from both agents (shared checkout):
- Mesh Party / Flare (experimental): phone-to-phone FIPS via party QR,
Flare chat/beam on :5680, optional fixed UDP listen (2121); party-only
phones get the baked-in Archipelago anchor so two bare phones reach
each other across the internet, not just on a hotspot.
- Keep-warm mesh: a running healthy node survives app opens (restart
only when dead or peers changed — peersDirty); requestMeshRestart for
config changes. Cold start measured on-device: session in ~3.1s.
- Restored after regression: startup LAN-vs-mesh race (the party work
was based on a pre-#114 file copy) and same-node routing (BTCPay/
Pine/HA had returned to opening in the external browser).
- UI: top status-bar scrim removed (bottom-only bars); web app-session
mobile bar is solid black — app theme colour no longer bleeds through
the bar and its safe-area strip.
Served APK: 0.5.7 (vc27).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements the app-side recommendations from the node diagnosis
(HANDOFF-2026-07-23): first session through the public tree takes 15s+
when it works at all, far beyond the old ~8s probe window.
- connect(): mesh ULA probed inside a 60s budget, 15s per-phase timeouts.
- ArchyVpnService: session warmer probes every saved node ULA as soon as
the tunnel is up (5s cadence first minute, then 60s keep-warm) so the
UI hits a warm session and it never idles out.
- Served APK: 0.5.5 (vc25); handoff updated with 25-ping evidence that
public-tree discovery currently fails outright (supports the
private-tree infra decision).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On-device diagnosis (phone on adb, 5G): mesh sessions established but no
packet ever entered the tunnel — Android hands the VpnService TUN fd over
non-blocking, and the fips fork's dedicated blocking-read thread treats
EAGAIN as fatal, dying at startup. archy-fips-core now forces the fd into
blocking mode before Node::start_with_tun_fd. Verified on-device: reader
survives, packets flow into the mesh, and the 30s anchor-link flap is
gone (stable 8+ min on 5G).
Also setMetered(false): Android 10+ defaults VPNs to metered, flipping
the phone into data-restricted behaviour whenever the mesh is up.
Served APK: 0.5.4 (vc24). Handoff updated with the remaining node-side
blocker: phone->anchor works, phone->node ULA gets no reply — needs
fipsctl introspection on Framework PT (suspect fork/daemon session or
routing mismatch).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The whole point of FIPS: the phone peers with the node's npub using the
LAN endpoint as a direct p2p dial hint AND rendezvouses via a public
anchor when away — neither LAN nor 5G location may matter. The anchor
half only existed when the scanned QR carried fanchors, so pairing
against an older node left the phone LAN-only and everything died on 5G.
The Archipelago vps2 anchor (npub + 146.59.87.168:8444/tcp, lockstep
with core fips/anchors.rs) is now baked into upsertNodePeer as a
guaranteed peer. Mesh connect retry window widened so a first-ever
pairing survives the VPN consent dialog. Served APK: 0.5.3 (vc23).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field test on 5G (Framework PT): pairing scanned fine but connect
hard-failed on the scanned LAN IP, and enabling the mesh VPN killed the
phone's internet.
- ArchyVpnService: the TUN is IPv6-only, and Android blocks every
address family the VPN holds no address for — allowFamily(AF_INET)
(+ allowBypass) so normal traffic flows while only fd00::/8 is routed.
- ServerConnectScreen.connect(): when the LAN address doesn't answer and
the entry carries a mesh ULA, bring the tunnel up (autoStartIfReady)
and probe the ULA with retries before reporting failure — the scanned
IP is a dial hint, the npub/ULA is the identity (npub-first contract).
- Served companion APK refreshed as 0.5.2 (vc22); handoff doc updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The APK served from the pairing QR predated today's scanner fixes, so
the download-then-scan flow still hit the laggy scanner. Bumps the
version so phones update in place, refreshes the served artifact
(signed v1+v2+v3, verified), and adds the archi-dev-box deploy handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three npub-bearing anchors pushed the pairing URI to ~600 chars (QR ~v23 at
EC-M) — phone cameras couldn't lock onto it off a screen. The phone only
needs the node itself (LAN p2p) plus one public rendezvous (vps2 preferred),
and screen scans don't need EC-M's damage tolerance — L drops a full
version tier.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- archipelago-gamepad-keys: evdev->uinput daemon (pure python3 stdlib) that
mirrors any attached controller as a virtual keyboard — D-pad/stick to
arrows, A/B to Enter/Escape, X/Y to Space/f, shoulders to Tab/Shift+Tab.
The browser sees real keys, so controllers work inside EVERY app iframe
(IndeeHub, Jellyfin…) with zero per-app code. Ships via the ISO splice and
bootstrap self-heal (kiosk nodes only), hotplug via 5s rescans.
Implements docs/tv-input-iframe-apps.md.
- useControllerNav: an open [role=dialog][aria-modal] owns navigation —
focus is pulled inside, arrows move spatially between its controls, Enter
activates, Escape stays with the modal's own close handling. One standard
mapping for every modal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>