Rewrites the prepared 1.7.117 section to include the mesh flash flow,
first-class Reticulum fixes, radio-first routing, mesh chat polish, the
transactions-modal phone fixes, services-vs-apps classification, the
lightning slow-payment fix, cached-resource page loads, load-shedding,
FIPS uptime hardening, and companion 0.5.25. What's New modal block
regenerated from the new bullets (sync-whats-new --check passes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A published port no longer implies a web UI. The package scanner used to
synthesize interfaces.main.ui="true" for any container with a port or
onion address, so headless backends — including self-deployed compose
stacks like podsteadr — showed up as launchable apps. New ui_detection
module decides instead: a manifest interfaces declaration (catalog
overlay first, disk second) is definitive; undeclared apps get a short
HTTP probe of the launch port (HTML page, redirect, or browser auth
wall = UI; JSON APIs, raw TCP, dead ports = service), with cached
verdicts and probes gated on running containers. Frontend canLaunch
now refuses curated services outright and only treats a bare runtime
address as launchable for curated known apps.
Works identically for manifest apps and containers deployed by hand
outside the orchestrator. ui_detection tests 6/6, frontend suite
696/696.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chips become sticky inside the modal's scroll region with a dimmed
blurred band, so the rail filter stays reachable while rows scroll
underneath. Verified in headless chromium: pinned at scroll-region top
after deep scroll, touch swipes starting on the chips still scroll.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the external flasher.meshcore.co.uk link with a button that
opens the global device-setup modal directly at its flash step, via a
new manual entry point in the mesh store (flashFlowPath). Manual opens
target the connected radio (else the first detected stick), skip the
read-only probe — the port is held by the live session and a second tty
opener corrupts it; the backend flash job stops the listener itself —
and close the modal instead of stepping back to the detection screen.
Button is disabled with a hint when no radio is present.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tx list kept a vestigial overflow-y-auto from before the modal
contract refactor made BaseModal's slot wrapper the scroller. With a
modal open, modal-scroll-locked applies overscroll-behavior:contain to
every .overflow-y-auto inside the overlay, so touch scrolls latched
onto the non-scrollable inner list and could not chain up to the real
scroller — the modal was unscrollable on any touch device. Wheel input
latches onto the scrollable ancestor directly, which is why desktop
never showed it. Verified with headless-chromium touch synthesis at
360x640/320x568: list scrolls to bottom, background stays contained.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post-merge regression from combining two individually-correct changes:
main's 2026-07-23 fix makes open_preferred_path bail WITHOUT touching the
port when no device_kind is pinned, while the hw-config branch's skip_path
dedup excludes the preferred path from the auto-detect fallback on the
assumption it was already probed this cycle. Together, on a single-radio
node with no pin (the common fleet state), the only candidate was never
probed at all and the mesh never came up — hit live on archi-dev-box
right after deploying merged main.
Fix: when device_kind is None, skip open_preferred_path entirely and go
straight to auto_detect_and_open with skip_path=None. The pinned path
keeps the existing probe-then-skip fallback.
Verified live on archi-dev-box: radio auto-detected, Reticulum daemon
ready, 5 persisted peers loaded. Mesh tests 116 passed / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Image quality modal: 'Send via' pills (LoRa / FIPS / Tor) when the
peer is federation-reachable — mesh.transport-advice now returns
has_fips + last_transport alongside has_tor. Picking FIPS/Tor routes
the image over the content-ref path instead of the radio.
- Attachment modals (transport chooser, image quality, new hop modal)
Teleport to body so the backdrop dims the FULL viewport — rendered
in-place they sat inside a transformed glass panel that trapped
position:fixed to the right chat panel.
- Click a message's transport pill → route modal: radio hops + live
SNR/RSSI quality for LoRa transports, overlay/circuit shape for
FIPS/Tor, delivery + E2E state.
- Reactions move behind a compact 'React ▾' dropdown with a larger
12-emoji palette.
- Unread badges now clear like a normal chat app: opening a contact
clears ALL twins of the merged conversation (badge sums every
contact_id — clearing just the clicked one left it stuck), and only
once the chat has scrolled to the latest messages; scrolled up into
history, new arrivals accumulate until you scroll back down.
- Refresh button shows only the spinner while refreshing (text+spinner
overflowed the fixed button width).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
E2E-verified both directions dev-box<->x250 over real RF (5KB image ~60s):
1. Sender daemon never called link.identify() — receiver's
get_remote_identity() was None, so every arrived transfer carried an
empty source_hash and the Rust side dropped it (now also warns
instead of silently vanishing it).
2. Receiver treated resource.data as bytes, but RNS hands a concluded
Resource's data as a file-like BufferedReader — b64encode raised
TypeError and the transfer was lost even when attributed.
3. Radio twins of merged contacts carry the peer's Archipelago ed25519
key as pubkey_hex, not an RNS hash — prefix lookup could never match
('Unknown Reticulum prefix', observed live). resolve_dest_hash now
falls back to matching the announce-bound arch_pubkey_hex.
4. The daemon RPC socket kept asyncio's default 64KiB line limit; any
attachment >~48KB overflowed it and tore down the whole daemon
connection ('reticulum-daemon is gone'). Raised to 16MiB.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both sides independently fixed the serial-alias dedup and the ESP32
boot-reset races; kept the branch's defer-to-auto-detect for unpinned
preferred paths (single probe pass per cycle) on top of main's
advert-name threading, Reticulum name propagation and radio-first
routing. Modal keeps main's 'Set Recommended' naming + probe progress
bar alongside the branch's in-app firmware flasher step.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two halves of the same twin-resolution gap, found live while testing
images between archi-dev-box and archy-x250-dev:
- peer_dest_prefix resolved the given contact row's own pubkey. For the
UI's merged conversation (the federation-synthetic id) that's the
Archipelago ed25519 identity key, NOT a radio routing key — so every
Reticulum resource send (images/files over LoRa) failed with 'Unknown
Reticulum prefix' while the UI showed the message as sent. It now
resolves through the radio twin (same arch identity, radio-range id).
- send_typed_wire sent EVERY federation-synthetic contact over the
federation path (FIPS→Tor), even with the same node one LoRa hop away.
Policy per operator: LoRa first when the payload fits and the radio
twin is reachable, then FIPS, then Tor. Verified live: text to the
merged contact now logs 'Radio-first routing' and lands with
transport=reticulum on the peer.
Also restyles the mesh-chat attachment download controls: the pre-fetch
button was a bare .btn that squished to text width in the narrow mobile
bubble; now a full-width glass pill with a download icon and fetch
spinner, and the on-image overlay swaps the emoji glyph for a crisp SVG
in a properly-sized glass circle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tests/mesh/run-mesh-tests.sh: cheap-first layers — 116 Rust mesh unit
tests, the daemon --selftest (now also asserting the announce app_data
wire contract and the set_name verb), and opt-in live assertions against
a running node (radio connected, named, mesh.refresh/broadcast, no
ARCHY-blob peer names). Verified green on archi-dev-box and archy-x250-dev.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Refresh button: real handler — calls the new mesh.refresh (radio
re-query) plus contacts/federation/outbox re-reads, disabled with a
spinner while running (was an unawaited cache repaint with no feedback
that skipped half the list's data sources).
- Broadcast button: success ('Sent ✓') and failure states with the error
in the tooltip; failures no longer vanish as unhandled rejections.
- The store's 5s status poll no longer wipes the error banner each tick.
- Contacts/aliases, federation nodes and the outbox badge refresh every
~30s (were mount-only and went permanently stale).
- Peer-list empty state keys on the merged list, so federation rows and
the channel rows still render with no radio attached.
- Device setup modal: 'Set Recommended' naming, probe progress bar with
stage labels instead of an anonymous spinner.
- Device panel: name save clears properly (empty = fall back to server
name) and the confirmation reflects the new live apply.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root causes found and fixed after live debugging on archi-dev-box (all
verified on real RNode hardware, archi-dev-box <-> archy-x250-dev E2E):
- probe_rnode raced the board's own boot: opening the port pulses DTR/RTS
through USB-UART bridges (CP2102/Heltec V3), the ESP32 power-cycles and
spends ~2.5-3s in boot ROM, and the KISS DETECT written 300ms after open
landed in the void — so an RNode could NEVER connect on these boards.
Now: immediate probe (fast path), then drain-until-quiet boot settle and
a second DETECT with a fresh response window.
- MeshService::configure() only restarted the listener on enable/disable —
device_kind/device_path/advert_name/RF-param changes were silent no-ops
until a full process restart (the setup modal's apply/keep-as-is did
nothing). Material config changes now bounce the listener; the open
sequence races the shutdown signal so stop() no longer burns the full
15s timeout mid-probe; mesh.configure applies in the background instead
of stalling every status poll behind the service write-lock.
- The mesh name was write-only: config.advert_name had no reader,
server.set-name never reached the mesh service, and Reticulum's
set_advert_name was a no-op (daemon display name fixed at spawn, and the
ARCHY:2 announce blob REPLACED the LXMF display name — every archy node
was anonymous on RNS). Now: advert_name > server name precedence feeds
the session, renames restart it live, the daemon gets --display-name at
spawn plus a set_name RPC verb, and announces carry the LXMF-standard
msgpack name with the identity blob appended as an extra list element
stock clients (Sideband/NomadNet) ignore.
- Dead reticulum-daemon was invisible for up to 30min (RX-stall watchdog):
child exit / RPC-EOF now fails try_recv_frame so the session reconnects.
- Setup modal re-trigger loop: plugged_at used the tty node's mtime, which
bumps on every open — each probe invalidated the dismissal key. Use
btime/ctime (only change on real plugs).
- ARCHY:2 identity adverts (re-emitted every 60s over Reticulum) stomped
the federation twin's real name with a synthetic Archy-… placeholder and
nulled its position; blob-only announces no longer assert a name, blob
strings can never become display names, and stale blob names are healed
at peers.json load.
- mesh.broadcast on Meshtastic sent heartbeat+time only (no identity);
SendAdvert now also fires a want_response NodeInfo broadcast.
- New mesh.refresh RPC: actively re-queries the radio contact table (the
UI Refresh button previously only re-read server caches).
- Reticulum peers now track last_advert (announce time) and mark existing
peers reachable on inbound traffic.
- Boot auto-enable no longer force-enables mesh when an operator
explicitly disabled it (only fires when no config file exists).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two FIPS containers on the Server page were the last network cards
still fetching-on-mount into local refs — every visit was a blank card
until fips.status / fips.list-seed-anchors answered. Both now ride the
cached-resource layer: FipsNetworkCard shares the server.fips-summary
key with the Local Network card's FIPS row (one fetch, never disagree),
seed anchors cache under server.fips-seed-anchors, and mutations
write the RPC's authoritative result straight into the cache. The 15s
status poll skips hidden tabs — revalidate-on-focus covers the return.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The HTTP accept loop parked on acquire_owned() when the connection
budget drained, freezing accept() for every client (the .228
session-flapping / CLOSE-WAIT signature). Permits drained because
half-open clients and hung upstreams held them indefinitely.
- try_acquire_owned + immediate 503-and-close when the budget is
exhausted; the accept loop itself never blocks
- 30s http1_header_read_timeout drops slowloris/half-open clients
- 900s watchdog bounds non-upgraded connections; websocket upgrades
are exempt (legitimately long-lived)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of the .198 load spiral (2026-07-28): needs_repair() called
ensure_image_present() every 30s tick to render the expected unit, so
under IO pressure the image-existence check timed out, read as "image
missing", and a 900s podman build ran inside the PROBE while the
companion was up — each build pegging the disk that made probes fail.
- needs_repair() is now build/pull-free: unit file present → service
is-active (10s cap; a hung systemctl reads as "assume active", never
as dead) → unit matches one of the three image refs install_one could
have written → context-newer-than-image staleness only when the unit
uses the auto-built :latest.
- Per-companion 10-min repair cooldown after a failed install_one, so a
failing build retries at most every REPAIR_COOLDOWN instead of every
reconcile tick.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bridge WebSocket patches into the resource layer: a /peer-health/<onion>
patch invalidates that peer's cloud.peer-browse entry and the federation
node list; /package-data patches invalidate the tor-services list.
invalidate() debounces 800ms and refetches only keys with mounted
subscribers, so patch storms cost one revalidation per key; the 30s
staleness reconciliation remains the backstop for unmapped data.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Credentials: identity.list + identity.list-credentials become
useCachedResource entries; explicit reloads still toast on failure.
- OpenWrtGateway: openwrt.get-status caches in the shared store (revisits
paint the last router state instantly); the connect flow's
params/No-router-configured semantics are preserved on top of the entry.
- ContainerApps assessed and left as-is: its Pinia store already persists
across navigation, keeps last data on error, and gates the spinner on
empty — same class as Apps/Marketplace/Fleet.
This closes the B4 rollout list from docs/FIPS-UPTIME-AND-UI-STATE-PLAN.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The keeps-data-visible-while-refreshing tests for PeerFiles, Server, and
LightningChannels mounted without Pinia (the converted components now
pull the resources store in setup) — add createPinia to the mounts.
LightningChannelsPanel: refresh the main channel list before the closed
history so the primary entry gets the first response, and null-guard
both fetchers' response shapes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
network summary (4-RPC allSettled aggregate), fips row, vpn peers,
interfaces, and tor services become useCachedResource entries — revisits
paint instantly, background refreshes keep content on screen. Mutations
write through the cache: DNS apply + the 15s vpn poll patch the network
aggregate via optimistic() instead of refetching all four RPCs; peer
removal filters the cached list. loading/refreshing flags derive from
entry loadState (drops the hand-rolled hasLoaded bookkeeping).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
federation nodes + dwn.status become useCachedResource entries: revisits
paint the node list instantly, `loading` fires on true first-load only
(the old showLoader semantics), the 5s poll refreshes silently like the
old surfaceErrors:false path, and explicit reloads after mutations still
surface failures in the error banner. Replaces the hand-rolled
loadNodesWithOptions SWR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lnd.listchannels (+summary) and lnd.closedchannels become separate
useCachedResource entries: reopening the panel paints the last channel
lists instantly and revalidates behind them; a closed-history failure
keeps its last list without touching the main view (same semantics as
the old nested try). Open/close mutations still force a refresh.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
monitoring.current/history/alerts/alert-rules become useCachedResource
entries: revisiting the page paints the last snapshot, chart, and alert
list instantly and the 5s poll revalidates behind them (refreshes dedup
in the store; errors keep last-known values instead of blanking).
Alert-rule toggles and acknowledgements refresh their entries after the
mutation as before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- lnd.getinfo and wallet.networking-profits become useCachedResource
entries (web5.lnd-info / web5.networking-profits): revisits paint
instantly from cache, errors keep last-known values, refreshes dedup.
- walletConnected is now derived from the lnd-info entry (with a manual
disconnect override preserving the connect/disconnect toggle).
- Drop the eager wallet.ecash-balance + lnd.gettransactions loaders and
their 30s polling — they fed only the hidden wallet card; the lnd-info
poll remains for the connected pill until B5 moves it to WS-push.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>
Phase A3 of docs/FIPS-UPTIME-AND-UI-STATE-PLAN.md (RC5), measured against
the A2 dial_stats baseline:
- 25s connectivity watcher in the fips supervisor: re-applies seed anchors
immediately on an anchor-link drop, on startup-disconnected, AND on
silent data-path death (connect_fails growing with zero fips_ok — the
live .198 failure where the daemon reported "connected" while every
dial blackholed and the 300s tick never healed it). Bounded to one
re-apply per 60s.
- anchors::apply is now concurrent with a 15s per-connect cap — the old
serial loop waited unbounded on each `sudo fipsctl connect`, so one
hung subprocess stalled the whole periodic tick.
- rebindable peer listener: the accept loop returns after persistent
accept errors (was: continue forever = inbound-dead until restart) and
peer_late_bind_loop rebinds — also on fips0 ULA change.
- is_service_active gets a 10s TTL cache (was up to 2 systemctl spawns
per dial attempt and per warm-tick peer).
- the warm tick now warms the union of federation peers + configured
seed anchors (direct anchor links used to go cold between 300s ticks),
skipping the redundant per-peer service check.
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>
Phase A2 of docs/FIPS-UPTIME-AND-UI-STATE-PLAN.md (RC6). Fallbacks to Tor
were debug!-only and uncounted, so "FIPS uptime" was unfalsifiable and
paths that were 100% Tor by construction went unnoticed for months.
- fips::telemetry: process-lifetime counters for FIPS successes and the
six fallback reasons (no_npub, service_inactive, dns_fail, connect_fail,
http_404, http_5xx), exposed as `dial_stats` in fips.status
- dial.rs: every fallback branch now counts + logs at info! with a
`reason` field (resolve/connect/status branches)
- PeerRequest::record_transport(data_dir): opt-in hook that writes the
transport actually used to federation storage off the hot path — wired
into the dial sites that never recorded (DWN sync ×3, mesh blob fetch,
federation deploy notify, onion-rotation notify, node messages via a
new send_to_peer data-dir param)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase A1 of docs/FIPS-UPTIME-AND-UI-STATE-PLAN.md — the five changes that
made FIPS fall back to Tor even when a FIPS path existed:
- RC0: the fips.d drop-in now opens PEER_PORT 5679 (was 80+8443 only, so
every hardened node firewalled peers' FIPS dials; 28k drops on .198)
- RC4: /blob/ and /dwn/ added to the peer-path allowlist — mesh file
sharing and DWN sync were 404 → 100% Tor by construction
- RC2-G2: lan_fips_anchors dials PUBLISHED_UDP_PORT (2121) instead of the
dead 8668, with a drift-guard test against the rendered daemon config
- RC2-G1: direct LAN peering actually runs now — mDNS TXT advertises the
FIPS npub, discovery calls set_fips_npub, and the anchor tick hydrates
npubs from federation storage for peers on older builds
- RC3: FIPS attempt budget is a hard cap (retry no longer doubles it) and
the 12 hot call sites get explicit fips_timeout fast-fail so Tor keeps
its full budget (browse-peer, preview, /blob, DWN, node-message,
rotation notifies)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consolidated plan from the deep repo review: credential rotation, secret
scrub, repo restructure, registry parameterization, docs overhaul, deep
code cleanup, and fresh-history publish mechanics.
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>
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>
Observed live on .116 after the dedup/backoff fixes: the kiosk browser's
hot-swap auto-probe (mesh.probe-device) still interleaved its handshakes
with the listener's open sequence on the same tty every backoff window —
Linux double-opens ttys silently, both handshakes corrupted each other,
and every collision's open() DTR/RTS-reset the board again. The retry
heuristic from 5f01ec31 narrowed but couldn't close the race.
A static PORT_OPEN_LOCK now covers the listener's whole device-open
sequence and each probe attempt, so exactly one prober touches the port
at a time. With this + the settle/backoff/dedup fixes, the .116 radio
completed its first MeshCore handshake in days (node-fa161601, 8
contacts) and the session has been stable since.
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>
/dev/mesh-radio is a udev symlink to a ttyUSB*/ttyACM* node that is also
in SERIAL_CANDIDATES, so one board was detected, probed and DTR/RTS-reset
twice per reconnect cycle (and shown twice in the UI):
- detect_serial_devices() dedupes candidates by canonical path, keeping
the stable /dev/mesh-radio name
- auto-detect fallback skips the preferred path it just probed this cycle
instead of immediately resetting the same board again
- probe_device's active-session guard compares canonical paths, so a
probe via the alias can no longer open the tty the live session holds
Together with the 2s boot-settle and stable-session backoff gate, this
takes a plugged-in CP2102/ESP32 board from up to 9 reset events per
cycle at a permanent 5s retry floor down to one probe sequence per
backoff window — enough for the radio to actually finish booting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extracted from beff5dd5 on archy-hwconfig (the rest of that commit is
flash-feature code staying on its branch): a device that connects then
drops within seconds — e.g. mid-boot-loop — kept resetting backoff to
5s forever, and every retry's open() toggles DTR/RTS which itself
resets ESP32-family boards. Backoff now only resets after a session
survives STABLE_SESSION_THRESHOLD (20s), so an unstable device gets
progressively longer quiet gaps to actually finish booting.
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>
- lnd.closedchannels: closed-channel history via /v1/channels/closed
- channel list now includes waiting-close and force-closing pending
channels with their closing_txid
- closechannel reads the close stream's first update with a dedicated
client instead of hanging until the closing tx confirms; returns the
closing txid in display byte order
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 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 companion reaches the node at its fips0 ULA, and the web UI builds app
links as http://[<ULA>]:<direct port> — but rootless-podman published ports
bind 0.0.0.0 only, so every app URL was refused over the mesh (:8334 first).
A reconcile loop in the backend mirrors public IPv4 listeners: any port
>=1024 on 0.0.0.0 without an IPv6 any-listener gets a v6-ONLY [::] forwarder
to 127.0.0.1, following /proc/net/tcp* so install/remove and hardcoded
companion ports are covered without touching a single container. Purely
additive: IPv4/LAN/Tor access paths are untouched, v6only listeners cannot
collide with or intercept v4 traffic, and foreign IPv6 listeners win.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phones on the FIPS mesh reach the node at http://[<fips0 ULA>], but every
shipped nginx config listened on IPv4 only — nothing answered [::]:80/443,
so mesh HTTP failed with the tunnel fully healthy (found live on
framework-pt during the vc24 5G session, 2026-07-23). Canonical conf gains
listen [::] lines and patch_nginx_conf self-heals deployed nodes (covers
the sites-enabled -> archipelago-http dev variant via the existing
canonicalize pass); validated by the existing nginx -t + rollback flow.
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>
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>
evdev->uinput gamepad-as-keyboard daemon on kiosk nodes (works in any
iframe, any origin) + shell-side focus handoff via useControllerNav;
CDP/per-app injection rejected. Implementation order included.
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>
Name fields sit at firmware-version-dependent offsets; when an offset lands
in binary data, from_utf8_lossy handed the UI replacement-character soup in
the mesh modals and settings panel. decode_mesh_name(): strict UTF-8 to the
first NUL, no control chars, else a readable fallback (short pubkey /
node-<id>).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--enable-features=OverlayScrollbar gives the thin auto-hiding scrollbars a
remote browser shows (Peers view had a permanent classic scrollbar on the
TV). Rides the existing kiosk self-heal + ISO splice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of silent HDMI on kiosk TVs (framework-pt, LG TV):
1) fresh ISOs installed NO audio stack even though the kiosk launcher
expects PipeWire-Pulse — add pipewire/pipewire-pulse/pipewire-alsa/
wireplumber/alsa-utils to the rootfs and put the archipelago user in
'audio' (linger user manager gets no logind seat ACLs on /dev/snd);
2) the kiosk's boot-time Xorg modeset races the i915→HDA audio-component
bind, the ELD notify is lost, and every HDMI profile stays
'available: no' forever. Verified live: one off/on modeset re-delivers
the ELD and WirePlumber immediately switches to HDMI.
New archipelago-audio-router daemon (same splice-from-configs pattern as
the kiosk, ISO + include_str! self-heal): routes the card to the best
available HDMI profile, keeps the default sink on it, migrates live
streams on hot-plug, unmutes (HDMI forced 100% — the TV owns volume), and
re-modesets a connected external output once when no ELD reports a
monitor. bootstrap::ensure_audio_stack() installs packages + router on
already-deployed kiosk nodes via OTA (apt through systemd-run — the
service sandbox keeps /usr and dpkg read-only). main.rs also spawns the
pine satellite keeper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HA stores a voice satellite as a pinned LAN IP and never re-resolves; when
the LAN renumbers (framework-pt: entry at 192.168.1.241, LAN now
192.168.63.0/24) the speaker silently drops forever. A periodic keeper
probes IP-pinned wyoming entries, sweeps the node's local /24s for the
satellite's port when one dies, rewrites core.config_entries (HA stopped
around the edit so its shutdown flush can't clobber it) and starts HA again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- payments.rs: an expired invoice can never succeed on retry, so the
error now says so and tells the payer the way out (ask the recipient
for a fresh invoice) instead of echoing LND's raw reason.
- middleware.rs: the error sanitizer masked every Lightning payment
failure as 'Operation failed. Check server logs' — but LND's reasons
('invoice expired', 'unable to find a path') are written for the
payer and are all actionable. 'Payment failed', 'Invalid payment
request' and 'Missing payment_request' now pass through, with a
regression test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Browsers only honor a transparent same-origin iframe canvas when the
embedder and the iframe agree on color-scheme; the neode-ui shell's
:root{color-scheme:dark} was forcing the AIUI frame opaque and hiding
the background art behind the chat.
Also adds neode-ui/vite.preview.config.mts (preview on :8100 against
the mock backend on :5959).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gateway crash-looped dialing bitcoind at host.archipelago:8332 (the
host gateway IP, where bitcoind does not listen). Root-cause fix, three
parts:
- fedimint-gateway manifest: --bitcoind-url now comes from
$FM_BITCOIND_URL, filled by a {{BITCOIN_HOST}} derived-env — works on
Knots, Core, or any future Bitcoin distro.
- fedimint manifest: same derived-env replaces the hardcoded
FM_BITCOIND_URL=http://bitcoin-knots:8332 environment entry.
- orchestrator: removed the hardcoded FM_BITCOIND_URL=bitcoin-knots
override that clobbered the derived-env, and bitcoin_host() now
accepts any running bitcoin*/bitcoin container (excluding -ui and
archy-* sidecars), preferring the known names in order.
Catalog regenerated + signed (nodes install from the signed catalog, so
the manifest fixes only reach them via this regen).
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>
The mesh listener spawns /usr/local/bin/archy-reticulum-daemon for
Reticulum sticks, but the ISO never shipped it (framework-pt connected
its RNode only after a hand-copy, 2026-07-22). Bundled from the build
host with a loud warning when absent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'Insufficient balance: need 80 sats, have 0 sats' reached the UI as
'Operation failed. Check server logs.' — the sanitizer allowlist didn't
know wallet errors. Also allowlists the calm Lightning still-starting
notice, which it would have swallowed the same way.
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>