Backend: parse_mesh_packet now decodes MeshPacket.rx_snr (field 8, float) and
rx_rssi (field 12, int32), and a new POSITION_APP branch decodes Position.
latitude_i/longitude_i (fields 1/2, sfixed32) -- all field numbers confirmed
against the canonical meshtastic/protobufs mesh.proto, not guessed. Threaded
through ParsedContact -> refresh_contacts -> MeshPeer (mirroring how
pkc_capable was wired for #17), so mesh.peers now surfaces real rssi/snr/lat/
lon instead of always-null. Fixed a real bug found along the way:
update_node_info's unconditional contact replace would have silently wiped
any already-tracked signal/position data on the next NodeInfo packet -- now
preserves it.
Frontend: mesh.ts's updateNodePositionsFromPeers() feeds real position data
into the SAME nodePositions map MeshMap.vue already renders from (parallel to
the existing Coordinate/Alert-message path) -- MeshMap.vue itself needed zero
changes, it was already built for this.
105/105 mesh tests pass (4 new: rx_snr/rx_rssi decode, position decode +
incomplete-field handling, full packet_to_inbound_frame integration).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a per-message transport badge to archy↔archy mesh chats and fixes the
long-broken E2E badge — both meshcore and meshtastic, styled like the existing
E2E pill.
Transport pill:
- New `MeshMessage.transport` ("lora"/"fips"/"tor"), surfaced in the UI beside
the E2E badge (Mesh.vue transportLabel() → Mesh/FIPS/Tor, mesh-styles.css).
- Sent LoRa → "lora"; sent federation → finalized to the real leg ("fips"/"tor")
once the background send resolves (req.send_json transport), via an id-keyed
store update.
- Received: a post-dispatch stamp on handle_typed_envelope_direct's output
(monotonic ids) tags both transports without threading through all 20 typed-
dispatch sites — radio wrapper stamps "lora", federation injector stamps the
peer's last_transport ("fips"/"tor", default tor; the inbound HTTP carries no
FIPS-vs-Tor signal).
- Plain native/channel LoRa frames → "lora"; channel broadcasts stay non-E2E.
E2E pill fix:
- `encrypted` was hardcoded false at every MeshMessage construction site, so the
UI badge (Mesh.vue `v-if="msg.encrypted"`) never showed. Now: federation
envelopes are E2E (identity-signed over an encrypted transport); the meshcore
native-DM receive path already had a real `encrypted` flag (now also tagged
with transport). meshtastic-PKI radio E2E flag threading is a noted follow-up.
Backend cargo check + frontend vue-tsc build both green. Needs a live radio +
multi-transport pass on .116/.228 to confirm end-to-end (see
project_transport_pill / project_meshtastic_parity).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A node reachable both over LoRa and federation has two MeshPeer rows (radio
twin: low contact_id + firmware key; federation twin: high contact_id +
archipelago key), and messages key by peer_contact_id split across the two ids
— so opening one twin shows an empty thread (the .120->.89 symptom).
- backend: new group_peer_twins() helper groups peers by arch_pubkey_hex (set on
BOTH twins by bind_federation_twins), keeps the radio id as the mesh-first
send target, and unions messages across all twin ids. Wired into
conversations.list / conversations.messages / mesh.contacts-list. +3 unit tests.
- frontend: the live chat list merges client-side (mergedPeers) and matched twins
by the "Archy-z6Mk..." advert prefix, which the Meshtastic device rename broke
(radio now advertises the server name). Merge by arch_pubkey_hex instead, which
the backend reliably sets on both twins. Expose arch_pubkey_hex on MeshPeer.
- fix unrelated stale test: EcashTransaction test missing the new `kind` field.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
- DMs now use native meshcore unicast (CMD_SEND_TXT_MSG) instead of @DM2 channel
broadcasts: private (E2E-encrypted to the recipient pubkey by firmware), off the
public channel, and decodable by stock clients. Plain text (split, not MC-chunked)
to non-archipelago contacts; typed envelopes to archy peers.
- !ai replies now DM the asker privately (RadioDm) instead of broadcasting on ch0.
- Auto contact-import: a heard advert (PUSH_CONTACT_ADVERT/0x80, 32-byte pubkey) is
added via CMD_ADD_UPDATE_CONTACT (0x09) so contacts appear without a flood advert.
- clear-all now DELETES firmware contacts via CMD_REMOVE_CONTACT (0x0F) instead of
blocklisting; blocking filter removed entirely. Wiped contacts return when reachable.
- Contact reachability: MeshPeer carries last_advert + reachable (path-based); UI shows
a reachability dot.
- Peers list: contact search box (filter by name/DID/npub/pubkey) with a clear button.
- send_message routes stock contacts as plain native text (fixes garbled envelopes).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- mesh: stop broadcasting ARCHY:2 identity on the public channel (startup + every advert tick); receive path still parses inbound. No more public-channel spam.
- mesh assistant: trigger on !ai/!ask typed in 1:1 chat (was only the dead AssistQuery path + bare channel text); route the reply transport-aware via MeshService::send_message (Tor for federation peers, LoRa for radio) through a new AssistChatReply event consumed at the server layer — fixes replies never reaching federation askers.
- mesh assistant: per-contact !ai allowlist (allowed_contacts) bypassing trusted_only; config + RPC + is_sender_allowed.
- fedimint-clientd manifest: network_policy open -> bridge (invalid value made the loader skip the whole manifest, so fmcd never ran and federations never joined/listed).
- ui: AI panel — Claude model dropdown (Haiku/Sonnet/Opus presets) + allowlist contact picker.
- ui: Settings — App Updates + App Registry moved under Account.
- ui: mesh chat — overscroll-behavior: contain so chat scroll no longer bleeds to the contacts panel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the assistant scheduler, MeshAssistantPanel UI, and the remaining
config-RPC / live-toggle / Ollama-detect wiring on top of Phase 1.x.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bundles the Phase 2b/3/4/5 work that accumulated across prior sessions
and the new attachment chunking router from this session. Everything
ships in one shot so the full mesh surface stays coherent on-wire.
Telegram primitives (variants 13–18, 20–22):
- Reply / Reaction / ReadReceipt / Forward / Edit / Delete
- Presence heartbeat + last-seen tracking
- ChannelInvite + ContactCard payload types
- MessageKey (sender_pubkey, sender_seq) as cross-transport identity
- Action menu, reply banner, edit banner, tombstones, (edited) marker
- Debounced auto-read-receipts on scroll + message arrival
Activated prototypes (Phase 4):
- PsbtHash send RPC
- Contacts CRUD (in-memory alias/notes/pinned/blocked)
- Outbox 📤 badge, rotate-prekeys button
- Chunked send fallback (MCIIXXTT framing) as auto-failover inside
send_typed_wire when a typed wire exceeds the LoRa per-frame budget
Unified inbox (Phase 1):
- conversations.list + conversations.messages RPCs (UI collapse deferred)
Attachment transport router (new this session):
- ContentInline variant 23 + ContentInlinePayload carrying file bytes
directly in the envelope for small files with no Tor path
- mesh.send-content-inline RPC — mirrors to local BlobStore, rides
send_typed_wire which auto-chunks over MCIIXXTT framing (~2.3 KB cap)
- mesh.transport-advice RPC as single source of truth for tier
decisions: auto-mesh / choose / tor-only / impossible
- Receive arm writes inline bytes to local BlobStore so the existing
content_ref card renderer handles both transports uniformly
- MeshState.blob_store field + order-independent propagation from
RpcHandler::set_blob_store / set_mesh_service
- Frontend handleAttachFile calls advice first, branches into silent
auto-send, transport-chooser modal, Tor-only path, or red error
- Transport modal with 📡 mesh / 🧅 Tor options + ETA + disabled
state when peer has no Tor reachability
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Meshcore direct unicast silently drops between our two Archy nodes
(firmware reports flood sends with resp_code=6 but nothing arrives).
Wrap DMs as channel-1 broadcasts with a [0xD1][dest_prefix(6)][inner]
header; receivers filter by prefix and dispatch the inner payload
through the existing typed/base64/chunk ladder. Shrink chunk body to
125B so the wrapper still fits the 160B LoRa budget. Auto-heal
routing: CMD_RESET_PATH (0x0D) any type-1 contact with path_len=0 on
refresh so floods take over. send_text now returns the firmware's
flood/direct mode flag for diagnostics.
Disable the 120s presence heartbeat broadcaster — its CBOR payload
was being re-echoed as plaintext by the shared repeater, spamming
every visible node with garbled "Archy-…: av�…fstatusfonline…"
messages on channel 0. mesh.broadcast-presence RPC stays registered
but no longer transmits. Re-enable only once presence moves off the
shared broadcast path.
Also: MeshState.cmd_tx behind RwLock so stop()→start() cycles don't
fail with "command channel already consumed"; MeshService.send_cmd
helper; drop_message_by_id for control envelopes that shouldn't
appear as Sent bubbles; self_advert_name reflected into MeshStatus
after set; path_len/flags parsed out of RESP_CONTACT.
Frontend: unified inbox merges mesh peers with federation nodes by
DID/pubkey/name; hide presence/read_receipt/edit/channel_invite/
contact_card from chat stream; publicChannel index → 1 to match the
new DM-via-channel routing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Replaces click-anywhere-on-bubble with a tiny ⋯ trigger in the meta row
that fades in on hover (always visible on touch devices). Outside-click
closes the menu, bubble gets a `menu-open` class so the trigger stays lit.
* Action menu gains Forward (any message) + Edit + Delete (own messages
only, delete is red). Reaction spinner + reply preview upgraded to handle
typed targets (attachment/invoice/location/alert) via summarizeForPreview.
* Pending-edit banner with ✎ icon mirrors the reply banner; Send flushes as
mesh.edit-message when pendingEdit is set.
* Forwarded bubbles render "↪ Forwarded from {orig_name}" header; tombstone
+ (edited) markers; pending-reply close button upsized (28px, red hover).
* Scroll + message-arrival watcher fires a debounced 400ms read receipt
with per-peer seq dedup so we never double-ack.
* Chat header: ⟲ rotate-prekeys button next to the shield badge; 📤 outbox
count when mesh.outbox reports queued messages. Blob-store test widget
removed and chat list now sorts by most-recent message timestamp.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mesh peer pubkeys (LoRa advert ed25519) differ from federation node
pubkeys (archipelago identity), so matching on pubkey always missed
and attachments >160B had no transport. Match on master DID instead;
also accept an explicit peer_onion override from the frontend, which
resolves the peer by display name against federation.list-nodes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tap a bubble to open an action menu with Reply + 6 quick reactions.
Reply stashes the target MessageKey and flips the Send button to
"Reply" mode, routing through mesh.send-reply. Reactions call
mesh.send-reaction immediately and render as chips under the target
bubble, collapsed per emoji with a count and self-highlight. Reaction
messages are filtered out of the main chat stream so they don't create
standalone bubbles. Reply bubbles show a "↳ quoted snippet" header
when the target is still in the local window.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Compose row gains a 📎 attach button that uploads the file via /api/blob
and calls mesh.send-content for the selected peer. Received content_ref
bubbles render as a caption+filename card with either an inline image
preview or a Download button that calls mesh.fetch-content and swaps in
the returned local_url.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds renderers for tx_relay, tx_relay_response, tx_confirmation,
lightning_relay, and lightning_relay_response message types so these
appear as rich cards in the chat stream. sendArchMessage now looks up
our own onion via getTorAddress and skips federation peers that match,
preventing the duplicate "echoed back to self" message we were seeing
on single-node test federations. Empty-federation error message is
also clearer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- F1: Guard connectWebSocket against concurrent calls with isWsConnecting flag
- F2: Serialize mesh send operations with sendQueue to prevent fetchMessages races
- F3: Add global Vue error handler with toast notification
- S1: Replace sudo podman with podman across all scripts (rootless Podman)
- S2: Add health-cmd to all 40 container run commands in first-boot-containers.sh
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New Discover.vue (app store redesign)
- Fleet.vue dashboard for .228
- MeshMap.vue component
- Fixed Discover.vue type errors (unused var, type predicate)
- Various UI updates (Apps, Dashboard, Marketplace, Mesh, Web5)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Frontend store (mesh.ts):
- Add typed message interfaces: InvoiceData, AlertData, CoordinateData,
SessionStatus, AlertStatus, MeshMessageTypeLabel
- New actions: sendInvoice, sendCoordinate, sendAlert, getSessionStatus,
rotatePrekeys
Mesh.vue UI:
- Typed message rendering in chat bubbles:
- Invoice: orange card with sats amount, memo, bolt11 preview, paid badge
- Alert: red card (emergency/dead_man) or blue (status), signed badge,
GPS link to OpenStreetMap
- Coordinate: blue card with lat/lng, label, OSM map link
- Block header: purple inline with chain icon
- Session badge in chat header: green shield (Double Ratchet),
yellow (static encryption), gray (none)
- Session status fetched on peer selection via mesh.session-status RPC
Mock backend:
- Messages now include message_type and typed_payload fields
- Mix of text, invoice (paid + unpaid), alert (emergency + status),
coordinate, and block_header messages for testing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>