- PeerFiles.vue reads the SAME `cloud.peer-browse:<onion>` entry Cloud.vue's
per-peer fan-in fills, so Cloud → peer files paints instantly from cache
and revalidates behind it; catalog/error/loading/transport are now
computed views over the store entry.
- preview-peer fan-out is capped at 3 concurrent with a queue (was one 30s
RPC per media item, all at once, unbounded) and aborts on unmount.
- browse + preview RPCs drop to maxRetries:1 — retry×3 turned one slow
peer into a 90s spinner.
- fix useCachedResource's interface types: `ReturnType<typeof computed<T>>`
resolves to the writable overload (WritableComputedRef), which broke
vue-tsc against the plain computed() returns; use ComputedRef<T>.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Part B3 of docs/FIPS-UPTIME-AND-UI-STATE-PLAN.md — the worst
fetch-on-every-navigation offender converted to the cached-resource layer:
- section counts / peer nodes / my files / paid items are cached resources:
revisits paint instantly, refresh happens behind the content
(sticky-ready), errors keep last-known data
- peer files: per-peer cached browse entries replace the all-or-nothing
Promise.allSettled — each peer's rows render the moment it answers, with
"still fetching from N peers" + unreachable counts; browse-peer runs
with maxRetries:1 so one dead peer costs its timeout once, not ×3
- peer cards get a live transport badge (FIPS green / Tor amber, with
measured latency) from the transport field the browse response already
carried — the per-peer FIPS-uptime view, for free
- cloud store: per-path listing cache with stale-while-revalidate
navigate() and a last-wins guard; CloudFolder no longer reset()s the
store on every folder entry (that wipe forced a spinner each time)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Part B1+B2 of docs/FIPS-UPTIME-AND-UI-STATE-PLAN.md. Foundation for pages
that render instantly from cache on revisit and revalidate in the
background, instead of unmount-refetch-spinner on every navigation.
- stores/resources.ts: keyed {data, loadState, fetchedAt, error} entries
with sticky-ready (never regress ready→loading), keep-last-value on
error, per-key in-flight dedup, sessionStorage snapshot hydrate,
debounced invalidate() fan-out, optimistic-update-with-rollback
- composables/useCachedResource.ts: SWR hook over the store — synchronous
hydrate, TTL-gated background revalidate, revalidate-on-focus,
abort-on-unmount fetcher signal
- rpc-client: AbortSignal support (aborts pending retries too), opt-in
in-flight dedup keyed method+params, per-call maxRetries override
- 10 tests covering the SWR semantics
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
- The session subtree now ALWAYS lives under <body>; inline panel mode is
emulated with a fixed-position rect synced from the layout placeholder
(ResizeObserver + resize), so toggling panel/overlay/fullscreen no longer
re-parents — and therefore no longer reloads — the app iframe.
- Display modes are strictly per-app: saved per-app pick → app default
(IndeeHub: fullscreen) → panel. The global fallback is gone, so one app's
mode can never change how another opens.
- The iframe takes focus when the app loads, so keyboard / the gamepad
bridge work inside it immediately (TV).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A pasted fixed-amount invoice auto-fills and locks the amount ('set by
invoice' — nothing to type, just review and send); zero-amount invoices get
an explicit 'enter how many sats' hint. Plus a clipboard Paste button on the
destination field (hidden where clipboard read can't work).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Paid sharing: AccessControl::Paid gains an accepted-methods list
(lightning/onchain/ecash/fedimint; empty = all, back-compat). Sellers pick
methods in ShareModal, gated on what the node can actually receive (LND
running/channel open, ecash wallet, fedimint joined) with an ⓘ that
explains exactly how to enable a missing rail. Enforced server-side (the
invoice/onchain mints refuse non-accepted methods; the serve gate only
honors tokens/hashes for accepted rails) and the buyer's pay modal only
offers what the seller accepts.
- Purchased music: the two remaining lightbox paths now use the bottom-bar
player — the immediate post-ecash-purchase viewer and the Paid Files
tab's window.open.
- Picture-in-picture buttons on the peer video player and the cloud media
lightbox (utils/pip.ts; Chromium/Safari, no-op elsewhere).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scan/upload chooser (already on main) keeps the first move; when the
Android companion's window.ArchipelagoQr bridge exists, 'Scan with
camera' opens the native CameraX modal instead of getUserMedia — which
the companion's plain-http origin doesn't even have, so the button now
shows there too. Decodes come back via window.__archyQrResult, status
lines mirror out via setStatus, and stopScanning closes the native
modal once a code is accepted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Another agent's feat/companion-3finger-native-scan: native wallet QR scanner
in the companion (WalletQrScannerModal.kt), three-finger menu gesture,
WebView file uploads, and the same every-open scan/upload chooser as a
dedicated pane with native-scanner handoff. Conflict in WalletScanModal.vue
resolved in the branch's favor — its choose-pane implementation replaces the
in-pane interstitial from 6502f13e (same UX, plus the native handoff).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scan modal now always lands on a chooser pane — Scan with camera /
Upload image / paste — instead of auto-starting the camera. When the
Android companion's ArchipelagoQr bridge is present, live scanning is
delegated to the native camera modal (WebView getUserMedia lags);
animated-QR progress and errors mirror onto it via setStatus.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Per-app default display mode: explicit user pick (remembered per app) ->
app default -> last global -> panel; IndeeHub defaults to fullscreen.
- data-controller-primary: a media file card's gamepad-select clicks the
card (audio -> bottom-bar player, video -> lightbox) instead of the
card's download link.
- Discovered-nodes list scales with the viewport (max(14rem,45vh)) instead
of cramming a long list into a fixed 224px box.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Owned/purchased and cloud audio never opens a lightbox: PeerFiles'
owned-content viewer and the Cloud/CloudFolder preview route audio to the
global bottom-bar player (video keeps the lightbox).
- Web5 shared-content 'Buy' no longer fires the node-ecash payment on
click: it opens a confirmation with balance -> price -> balance-after,
and pays only on explicit confirm.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Pasting an invoice/address now gets the same second-step confirmation the
scan flow has: method, destination, live balance -> amount -> balance
after (invoice-fixed amounts win over the typed one), for every rail
including token minting. Nothing pays until Confirm & Send.
- The scan modal opens on a chooser every time: scan with camera or
upload/take a photo of the QR — the camera no longer auto-starts, and
Back only relights it if camera was the user's choice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pairing QR was effectively IP-first: the phone dialed the scanned LAN
host and went dark when the LAN renumbered or it left home. FIPS peers on
npubs; IPs are only dial hints. fanchors now leads with the paired node
ITSELF (fnpub @ current host, npub-keyed so LAN contact is direct p2p over
FIPS and survives DHCP), and fips.pair-info always includes the Archipelago
vps2 public anchor (pairing hint only — the node's own anchor file is not
modified) so the phone can rendezvous through the public mesh when away.
Contract doc updated with the REQUIRED app-side changes (built on the Mac).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The double-share/double-pay chain (user hit it live, 2026-07-22):
ShareModal's already-shared lookup compared the slash-stripped stored
path against the leading-slash filepath — never matched — so every
re-share minted a NEW catalog entry with a new id, and the buyer's
owned-guard (keyed by id) saw the duplicate as unowned and paid again.
Three independent walls now: (1) ShareModal normalizes both sides so
re-shares reuse the entry; (2) content_server::add_item dedupes by
filename server-side (updates in place, keeps the id so buyers' owned
records stay valid); (3) the buyer REFUSES to pay for content it
already owns — matched by (onion, content_id) OR (onion, filename) —
and serves the cached copy instead, before any ecash is minted.
Purchases also auto-file into Photos/Music/Documents on the node (same
buckets as the Cloud view, collision-safe naming) so bought files show
up where files live on every device, and Cloud gains a Paid Files tab
listing every purchase (name, size, sats paid, date) with in-app view.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- LND watchdog: framework-pt's LND sat wedged 14h (RPC up, server never
ready, channels inactive) with nothing noticing. 15 consecutive bad
minutes now bounce the container automatically (30min cooldown) —
silent multi-hour Lightning outages become self-healed blips.
- Send modal: Auto tab hidden; Ecash splits into Cashu and Fedi (new
wallet.fedimint-send RPC wrapping the existing spend_from_any); both
rails render the generated token as a scannable QR alongside the
copyable text.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On the companion (plain-http LAN = no secure context = no live camera)
the photo path IS the scanner, and qr-scanner's single wasm pass often
misses the dense QR of a full BOLT11 invoice. Android WebView's native
BarcodeDetector handles them easily — try it first, wasm as fallback,
plus a 'Reading photo…' status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Channels panel's 'Funding tx in Mempool' link still hardcoded the
local app (missed in the explorer rollout): now it routes like every
other tx link — local Mempool when running, else the saved external
explorer, with the first-time consent modal setting it up and then
opening the tx. The consent modal moves to z-3600 so it renders above
the Wallet Settings modal that can trigger it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Outside-close listeners move from click to pointerdown: pointerdown
fires before the open-toggle re-render, so the kiosk's slow software
compositing can no longer detach the click target mid-flight and close
the menu the instant it opens. color-scheme:dark (CSS + meta) pins every
native control — select popups, scrollbars, pickers — dark regardless of
the user's OS theme; explicit select/option colors cover Firefox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reachability probes ran one at a time with a 30s Tor timeout each, so
Refresh sat disabled for N-offline-peers × 30s. Now the list renders and
the button re-enables immediately; probes run concurrently and fill the
dots in as they land.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BaseModal becomes a flex column: title row and the new #header slot
(tabs) stay fixed at the top, #footer (action buttons) fixed at the
bottom, only the default slot scrolls (default max-h 90vh unless the
caller sets one). Wallet Settings migrates: tabs pinned, four duplicated
in-pane Close rows collapse into one pinned footer that carries the
active tab's primary action.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>