From eeb3c77e129078b0dda85a4582531bf5b2c22add Mon Sep 17 00:00:00 2001 From: Dorian Date: Tue, 17 Mar 2026 02:34:37 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=203=20Week=207=20=E2=80=94=20type?= =?UTF-8?q?d=20message=20UI,=20session=20badges,=20rich=20chat=20cards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- neode-ui/mock-backend.js | 24 ++++---- neode-ui/src/stores/mesh.ts | 111 ++++++++++++++++++++++++++++++++++++ neode-ui/src/views/Mesh.vue | 109 +++++++++++++++++++++++++++++++++-- 3 files changed, 228 insertions(+), 16 deletions(-) diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index 1267a6a2..7b9af3de 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -1520,18 +1520,18 @@ app.post('/rpc/v1', (req, res) => { const limit = params?.limit || 100 const now = Date.now() const allMessages = [ - { id: 1, direction: 'received', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Node online. Bitcoin Knots synced to tip.', timestamp: new Date(now - 3600000).toISOString(), delivered: true, encrypted: true }, - { id: 2, direction: 'sent', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Good. Electrs index at 98%. Channel capacity 2.5M sats.', timestamp: new Date(now - 3540000).toISOString(), delivered: true, encrypted: true }, - { id: 3, direction: 'received', peer_contact_id: 2, peer_name: 'satoshi-relay', plaintext: 'ARCHY:2:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2:d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5', timestamp: new Date(now - 3000000).toISOString(), delivered: true, encrypted: false }, - { id: 4, direction: 'received', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Federation state sync complete. 3 containers matched.', timestamp: new Date(now - 1800000).toISOString(), delivered: true, encrypted: true }, - { id: 5, direction: 'sent', peer_contact_id: 4, peer_name: 'bunker-alpha', plaintext: 'Running mesh-only mode. No internet for 48h. All good.', timestamp: new Date(now - 900000).toISOString(), delivered: true, encrypted: true }, - { id: 6, direction: 'received', peer_contact_id: 4, peer_name: 'bunker-alpha', plaintext: 'Copy. Block height 890,412 via compact headers. 6 confirmations on last tx.', timestamp: new Date(now - 840000).toISOString(), delivered: true, encrypted: true }, - { id: 7, direction: 'received', peer_contact_id: 2, peer_name: 'satoshi-relay', plaintext: 'New block relayed: 890,413. Fees averaging 12 sat/vB.', timestamp: new Date(now - 600000).toISOString(), delivered: true, encrypted: true }, - { id: 8, direction: 'sent', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Opening 1M sat channel to your node. Approve?', timestamp: new Date(now - 300000).toISOString(), delivered: true, encrypted: true }, - { id: 9, direction: 'received', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Approved. Waiting for funding tx confirmation.', timestamp: new Date(now - 240000).toISOString(), delivered: true, encrypted: true }, - { id: 10, direction: 'received', peer_contact_id: 3, peer_name: 'mountain-node', plaintext: 'Anyone copy? Solar panel restored, back online.', timestamp: new Date(now - 120000).toISOString(), delivered: true, encrypted: false }, - { id: 11, direction: 'sent', peer_contact_id: 3, peer_name: 'mountain-node', plaintext: 'Copy mountain-node. Welcome back. Relaying your backlog.', timestamp: new Date(now - 60000).toISOString(), delivered: true, encrypted: false }, - { id: 12, direction: 'received', peer_contact_id: 4, peer_name: 'bunker-alpha', plaintext: 'Dead man switch check-in. All systems nominal. Battery 78%.', timestamp: new Date(now - 30000).toISOString(), delivered: true, encrypted: true }, + { id: 1, direction: 'received', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Node online. Bitcoin Knots synced to tip.', timestamp: new Date(now - 3600000).toISOString(), delivered: true, encrypted: true, message_type: 'text' }, + { id: 2, direction: 'sent', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Good. Electrs index at 98%. Channel capacity 2.5M sats.', timestamp: new Date(now - 3540000).toISOString(), delivered: true, encrypted: true, message_type: 'text' }, + { id: 3, direction: 'received', peer_contact_id: 2, peer_name: 'satoshi-relay', plaintext: 'Block #890,413 relayed. Fees avg 12 sat/vB.', timestamp: new Date(now - 3000000).toISOString(), delivered: true, encrypted: true, message_type: 'block_header', typed_payload: { alert_type: 'block_header', message: 'Block #890,413 — 2,847 txs, 12 sat/vB avg fee', signed: true } }, + { id: 4, direction: 'received', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Invoice: 50,000 sats — Channel opening fee', timestamp: new Date(now - 1800000).toISOString(), delivered: true, encrypted: true, message_type: 'invoice', typed_payload: { bolt11: 'lnbc500000n1pjmesh...truncated...', amount_sats: 50000, memo: 'Channel opening fee', paid: false } }, + { id: 5, direction: 'sent', peer_contact_id: 4, peer_name: 'bunker-alpha', plaintext: 'Running mesh-only mode. No internet for 48h. All good.', timestamp: new Date(now - 900000).toISOString(), delivered: true, encrypted: true, message_type: 'text' }, + { id: 6, direction: 'received', peer_contact_id: 4, peer_name: 'bunker-alpha', plaintext: 'Copy. Block height 890,412 via compact headers.', timestamp: new Date(now - 840000).toISOString(), delivered: true, encrypted: true, message_type: 'text' }, + { id: 7, direction: 'received', peer_contact_id: 3, peer_name: 'mountain-node', plaintext: 'EMERGENCY: Solar array failure. Running on battery reserve.', timestamp: new Date(now - 600000).toISOString(), delivered: true, encrypted: false, message_type: 'alert', typed_payload: { alert_type: 'emergency', message: 'Solar array failure. Running on battery reserve. ETA 4h before shutdown.', coordinate: { lat: 39507400, lng: -106042800, label: 'Mountain relay site' }, signed: true } }, + { id: 8, direction: 'sent', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Opening 1M sat channel to your node. Approve?', timestamp: new Date(now - 300000).toISOString(), delivered: true, encrypted: true, message_type: 'text' }, + { id: 9, direction: 'received', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Approved. Waiting for funding tx confirmation.', timestamp: new Date(now - 240000).toISOString(), delivered: true, encrypted: true, message_type: 'text' }, + { id: 10, direction: 'sent', peer_contact_id: 3, peer_name: 'mountain-node', plaintext: 'Location shared', timestamp: new Date(now - 120000).toISOString(), delivered: true, encrypted: false, message_type: 'coordinate', typed_payload: { lat: 30267200, lng: -97743100, label: 'Supply drop point' } }, + { id: 11, direction: 'received', peer_contact_id: 4, peer_name: 'bunker-alpha', plaintext: 'Dead man switch check-in. All systems nominal. Battery 78%.', timestamp: new Date(now - 60000).toISOString(), delivered: true, encrypted: true, message_type: 'alert', typed_payload: { alert_type: 'status', message: 'All systems nominal. Battery 78%. Mesh uptime 14d.', signed: true } }, + { id: 12, direction: 'received', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Invoice paid: 50,000 sats', timestamp: new Date(now - 30000).toISOString(), delivered: true, encrypted: true, message_type: 'invoice', typed_payload: { bolt11: 'lnbc500000n1pjmesh...truncated...', amount_sats: 50000, memo: 'Channel opening fee', paid: true, payment_hash: 'a1b2c3d4e5f6...' } }, ] return res.json({ result: { diff --git a/neode-ui/src/stores/mesh.ts b/neode-ui/src/stores/mesh.ts index a4e49869..8934ba0f 100644 --- a/neode-ui/src/stores/mesh.ts +++ b/neode-ui/src/stores/mesh.ts @@ -35,6 +35,14 @@ export interface MeshChannel { has_secret: boolean } +export type MeshMessageTypeLabel = + | 'text' + | 'alert' + | 'invoice' + | 'psbt_hash' + | 'coordinate' + | 'block_header' + export interface MeshMessage { id: number direction: 'sent' | 'received' @@ -44,6 +52,46 @@ export interface MeshMessage { timestamp: string delivered: boolean encrypted: boolean + message_type?: MeshMessageTypeLabel + typed_payload?: InvoiceData | AlertData | CoordinateData | null +} + +export interface InvoiceData { + bolt11: string + amount_sats: number + memo: string | null + payment_hash?: string + paid?: boolean +} + +export interface AlertData { + alert_type: 'emergency' | 'status' | 'dead_man' | 'block_header' + message: string + coordinate?: { lat: number; lng: number; label?: string } + signed?: boolean +} + +export interface CoordinateData { + lat: number + lng: number + label?: string +} + +export interface SessionStatus { + has_session: boolean + forward_secrecy: boolean + message_count: number + ratchet_generation: number + peer_did: string | null +} + +export interface AlertStatus { + dead_man_enabled: boolean + dead_man_interval_secs: number + triggered: boolean + time_remaining_secs: number + has_gps: boolean + emergency_contacts: number } export const useMeshStore = defineStore('mesh', () => { @@ -163,6 +211,64 @@ export const useMeshStore = defineStore('mesh', () => { } } + async function sendInvoice(contactId: number, amountSats: number, memo?: string) { + try { + sending.value = true + error.value = null + return await rpcClient.call<{ sent: boolean; amount_sats: number; bolt11: string }>({ + method: 'mesh.send-invoice', + params: { contact_id: contactId, amount_sats: amountSats, memo }, + }) + } catch (err: unknown) { + error.value = err instanceof Error ? err.message : 'Failed to send invoice' + throw err + } finally { + sending.value = false + } + } + + async function sendCoordinate(contactId: number, lat: number, lng: number, label?: string) { + try { + sending.value = true + error.value = null + return await rpcClient.call<{ sent: boolean }>({ + method: 'mesh.send-coordinate', + params: { contact_id: contactId, lat, lng, label }, + }) + } catch (err: unknown) { + error.value = err instanceof Error ? err.message : 'Failed to send coordinate' + throw err + } finally { + sending.value = false + } + } + + async function sendAlert(message: string, alertType: string, broadcast = false, lat?: number, lng?: number) { + try { + error.value = null + return await rpcClient.call<{ sent: boolean; signed: boolean }>({ + method: 'mesh.send-alert', + params: { message, alert_type: alertType, broadcast, lat, lng }, + }) + } catch (err: unknown) { + error.value = err instanceof Error ? err.message : 'Failed to send alert' + throw err + } + } + + async function getSessionStatus(contactId: number): Promise { + return rpcClient.call({ + method: 'mesh.session-status', + params: { contact_id: contactId }, + }) + } + + async function rotatePrekeys() { + return rpcClient.call<{ rotated: boolean; one_time_prekeys: number }>({ + method: 'mesh.rotate-prekeys', + }) + } + async function refreshAll() { await Promise.all([fetchStatus(), fetchPeers(), fetchMessages()]) } @@ -185,5 +291,10 @@ export const useMeshStore = defineStore('mesh', () => { refreshAll, markChatRead, clearViewingChat, + sendInvoice, + sendCoordinate, + sendAlert, + getSessionStatus, + rotatePrekeys, } }) diff --git a/neode-ui/src/views/Mesh.vue b/neode-ui/src/views/Mesh.vue index 2e39f50e..6cf7c9ce 100644 --- a/neode-ui/src/views/Mesh.vue +++ b/neode-ui/src/views/Mesh.vue @@ -1,8 +1,8 @@