Unread badges came back on every visit (framework showed a phantom "2"
with nothing new): unreadCounts was memory-only, so each page load
replayed the entire message history as "new". Seen-state now persists
as a per-contact highest-seen-message-id watermark in localStorage
(ids are backend-monotonic across restarts); first run after this
ships seeds the watermark from history so nobody gets a wall of stale
badges. Opening a chat advances and persists the watermark for all
twins of the merged conversation.
The hop-route modal the user asked for is now reachable from a visible
per-message "⋯" button (the transport pill remains clickable too), has
a fallback title/branch for messages that predate transport tracking,
and animates: endpoints and link reveal in sequence, a pulse travels
the link, and relay dots blink in order — all disabled under
prefers-reduced-motion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v1.7.117 broke Reticulum mesh on OTA-only fleet nodes two ways: the
update swaps only the backend binary and frontend tarball, so nodes
kept a stale archy-reticulum-daemon whose argparse exits on the new
--enable-transport flag (mesh session died on every spawn — confirmed
on framework-pt), and they never had archy-rnodeconf at all, so the
in-app Flash LoRa flow failed with a bare "No such file or directory".
Four-part fix:
- The Rust supervisor probes `daemon --help` and only passes
--enable-transport when the daemon advertises it; unsupported daemons
run edge-only exactly as pre-1.7.117 (tested against stub daemons
both ways + missing-binary fail-safe).
- Both PyInstaller tools ride the frontend tarball's runtime payload
(radio-tools/) and bootstrap.rs promotes them to /usr/local/bin on
startup when bytes differ — the first OTA path that ever updates
them. create-release.sh now rebuilds them every release and the
manifest script hard-fails if they're missing.
- The ISO bundles archy-rnodeconf alongside the daemon (it never did).
- Flash LoRa reports "tool not installed — update the node" instead of
the bare spawn error when rnodeconf is absent everywhere.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Reticulum daemon gains --enable-transport, which writes
enable_transport = yes into the RNS config it regenerates on every
start, and the Rust supervisor always passes it. Archy nodes now relay
RNS traffic and rebroadcast announces, so archy nodes (and Sideband/
NomadNet peers) beyond direct RF range discover and reach each other
through any archy node in between — edge-only operation left every
node limited to its own radio horizon. RNS's per-interface airtime
caps bound the extra announce overhead on LoRa.
Verified: config generation with the flag on/off, daemon --selftest
green with transport enabled, mesh test module 116/116.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>