UI (this session):
- Global audio player now scales the whole interface into the space above it
on desktop (sidebar + main) and docks directly above the tab bar on mobile;
it stays visible while navigating.
- Mesh mobile redesign: floating Chat / BTC / Dead Man / AI / Map tab strip
with a single fixed, internally-scrolling pane (page no longer scrolls);
tabs hide while a conversation is open; floating back button; collapsible
Device panel (starts collapsed); keyboard-aware conversation sizing via
VisualViewport so the chat sits just above the keyboard.
- Cloud file grid: uniform 4/3 card heights (folders + images match).
- Swipe left/right switches tabs on the Apps and Web5 screens.
- Map tool fills its pane (no bottom gap); fix skewed Share Location toggle
on mobile (global min-height rule was deforming the switch).
- Trim redundant helper copy from the mesh AI tab.
Also bundles pre-existing in-progress work that was already in the tree:
mesh listener/session + wallet + container + bitcoin-status backend changes,
docker UI updates, and assorted other UI tweaks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Large peer downloads (~178MB) failed with a generic 'Operation failed', and
the download path had three stacked problems:
- The FIPS reqwest client used a hard-coded 20s total timeout regardless of the
caller's .timeout(), so a big transfer over the mesh aborted at 20s before
the Tor fallback could help. Honor the per-request timeout (client_with_timeout).
- The peer-content proxy buffered the whole file into node memory via
resp.bytes() before sending a byte, and capped the transfer at 60s. Stream
the body through with hyper::Body::wrap_stream (constant memory) and raise the
timeout to 900s; bump the nginx peer-content read timeout to match.
- Free downloads pulled the file as base64 over RPC, doubling it in node memory
and the browser — fatal for large files. Download free files by streaming
from /api/peer-content straight to disk, after a 1-byte Range probe that
surfaces the real reason (peer offline on mesh and Tor) instead of a generic
failure. Paid downloads now return the real error through the {error} channel
the UI already displays.
Adds the reqwest 'stream' feature for bytes_stream().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User priority: FIPS is the main transport but it was unreliable and needed a
manual "Activate" button. Improvements (all in the FIPS dial/supervisor):
- Auto-activate: ensure_activated() installs the daemon config + starts the
service on its own once seed onboarding has materialised the key — no Activate
button needed. Idempotent; runs from the supervisor every 45s so a node that
onboards after boot still comes up automatically.
- Dial retry: try_fips_get/post now retry ONCE on a connect/timeout error. The
first dial to a peer triggers NAT hole-punching and often times out before the
path is up; the retry lands on the now-warm path — the main reason calls were
dropping to Tor despite the peer being FIPS-reachable.
- More patient connect_timeout (5s→8s) so a reachable-but-cold peer isn't
abandoned to Tor while hole-punching completes.
- Path warmer: spawn_fips_supervisor() keeps hole-punched paths to known
federation peers warm (every 45s, concurrent), so on-demand dials are fast and
land on FIPS.
- Confirmed the daemon config already enables BOTH udp + tcp transports
(render_config_yaml), so FIPS already uses TCP where UDP is blocked; the Tor
fallback was path-establishment, addressed above.
cargo check + fmt clean. Backend — needs a binary rebuild+deploy to validate on
.116/.198 (watch last_transport flip fips, and FIPS coming up with no button).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FIPS peer content browse over the mesh was failing with "Peer returned
error: 404 Not Found" and never falling back to Tor. `is_peer_allowed_path`
only allowed `/content/<id>` (item fetches) — the catalog endpoint is
exactly `/content` (no trailing slash), so it 404'd over the FIPS peer
listener. A FIPS 404 was also treated as a successful response, so the dial
never retried Tor. Fixes: allow `/content` over the mesh; add
`fips_should_fall_back()` so a FIPS 404/5xx in Auto mode falls back to Tor
(handles version-skew peers reaching a different route). Also correct the
reconnect hint text — the public anchor is TCP/8443, not UDP/8668.
Federation: deleted nodes reappeared because transitive discovery
(`merge` of a peer's advertised trusted peers) re-added any unknown DID.
Add a tombstone store (`removed-nodes.json`): remove_node tombstones the
DID, transitive merge skips tombstoned DIDs, and a remote-triggered
peer-joined is ignored for a removed DID. Explicit local re-add (add_node)
clears the tombstone.
UI: the app credentials modal panel stretched edge-to-edge (height:100%,
max-width:none, items-stretch overlay). Constrain it to a centered card
(max-width 34rem, rounded, dimmed full-screen backdrop) matching the
AppIconGrid / wallet-receive modal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a user-configurable toggle for how each peer-to-peer service
reaches federated peers. Three options per service:
- Auto (default) — FIPS preferred, Tor fallback (current behavior).
- FIPS only — fail rather than fall through to Tor.
- Tor only — explicit opt-in to onion anonymity for that service.
Services covered (matching the UI rows):
- Federation — state sync, invites, peer notifications
- Peers — address/DID rotation broadcasts
- Peer Files — content catalog download/browse/preview
- Messaging — archipelago channel + mesh bridge
- Mesh File Sharing — content_ref blob fetches
Implementation:
- settings::transport — persisted struct + process-wide OnceLock handle
(so deep call sites don't need data_dir threaded through signatures).
On-disk file: <data_dir>/settings/transport_preferences.json; missing
or corrupt → defaults (Auto everywhere).
- settings::transport::init() called from main.rs after config load.
- fips::dial::PeerRequest gains a .service(kind) builder; send_* checks
the preference before choosing a transport. FIPS-only fails loudly
when FIPS is unavailable (so users who pick it know when something
falls back).
- Every FIPS-first migration site tags its PeerRequest with the
matching PeerService so the toggle actually applies.
- transport.preferences + transport.set-preference RPCs added; wired
into the dispatcher.
- neode-ui/src/views/settings/TransportPrefsCard.vue — standalone card
with a 5-row Auto/FIPS/Tor tri-state. Not wired into Settings.vue —
the user places components themselves (see feedback_ui_entry_points).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every federation peer-to-peer call now prefers FIPS (direct ULA dial
over `fips0`, ~LAN latency) and falls back to Tor only on network
failure. Per-method ed25519 signatures are preserved on both
transports so authenticity doesn't change.
- fips::dial::PeerRequest — fluent builder that owns transport
selection. Returns the Response plus the TransportKind that carried
it, so handlers can log or expose which path was used.
- fips::dial::is_service_active — free-standing async probe used by
migration sites (the transport::fips::is_available cache is keyed
to a `&self`, not usable from static contexts).
- federation/sync.rs: sync_with_peer + deploy_to_peer drop the
hand-rolled reqwest::Proxy dance, call PeerRequest instead.
- federation/invites.rs: notify_join takes the remote's fips_npub
(already parsed out of the invite code since v1.4) and dials over
FIPS when available. The "peer-joined" signature domain is
unchanged.
- api/rpc/federation/handlers.rs: DID rotation broadcast loops over
federated peers through PeerRequest; the per-peer result payload
gains a `transport` field so the UI can surface mesh vs. onion.
- api/rpc/tor/mod.rs: onion-address-change propagation is now the
most useful FIPS-first call — fips_npub is stable across onion
rotation, so peers get the new address even when the old onion
is already dead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the FIPS transport end-to-end so peer-to-peer calls can reach
other nodes over the mesh without going through Tor:
- fips::dial — raw RFC 1035 DNS client (zero new deps) that queries the
FIPS daemon's local resolver at 127.0.0.1:5354 for `<npub>.fips` AAAA
records. Exposes peer_base_url(npub) → "http://[fd9d:…]:5679" plus a
reqwest client factory for call-site migrations.
- fips::iface — parses /proc/net/if_inet6 to find the ULA address on
`fips0`. Runs under the archipelago service user without extra caps.
- FipsTransport::is_available() — live probe of archipelago-fips and
upstream fips.service via `systemctl is-active`, cached 10s so the
send hot path doesn't thrash DBus.
- FipsTransport::send() — resolve npub, POST TransportMessage JSON to
the peer's /transport/inbox. Today /transport/inbox isn't wired on
the receive side, so call-site migrations use dial::peer_base_url
directly against the already-signed endpoints (/rpc/v1,
/archipelago/node-message, /content/*). The inbox handler lands as
part of the Settings/transport work.
- server::serve_with_shutdown — takes an optional peer_addr and spawns
a second listener bound specifically to the fips0 ULA on port 5679.
The peer listener applies is_peer_allowed_path() — a whitelist of
endpoints that already do per-request signature auth — and returns
404 for everything else. Shutdown cascades to both listeners via a
watch channel; 5s drain window preserved.
- main.rs — if fips0 has a ULA at startup, pass the peer SocketAddr to
serve_with_shutdown; otherwise run the main listener only.
Security: the peer listener is bound to the fips0 ULA directly, not
wildcard, so it's unreachable from WAN IPv6. The path whitelist limits
exposure to endpoints whose handlers verify ed25519 signatures or
federation DID headers server-side.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>