From b514b05063090fffa78f1c98f3332e1e6fa44217 Mon Sep 17 00:00:00 2001 From: Dorian Date: Tue, 17 Mar 2026 03:31:44 +0000 Subject: [PATCH] fix: sync latest frontend source with linter fixes --- neode-ui/mock-backend.js | 113 +++++++++++++++++--- neode-ui/src/stores/mesh.ts | 111 +++++++++++++++++++ neode-ui/src/views/AppSession.vue | 12 ++- neode-ui/src/views/Apps.vue | 171 +++++++++++++++++------------- neode-ui/src/views/Mesh.vue | 109 ++++++++++++++++++- 5 files changed, 423 insertions(+), 93 deletions(-) diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index d02cd91..7b9af3d 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: { @@ -1570,6 +1570,95 @@ app.post('/rpc/v1', (req, res) => { return res.json({ result: { configured: true } }) } + case 'mesh.send-invoice': { + console.log(`[Mesh] Send invoice: ${params?.amount_sats} sats to contact ${params?.contact_id}`) + return res.json({ + result: { + sent: true, + message_id: Math.floor(Math.random() * 10000) + 200, + amount_sats: params?.amount_sats, + bolt11: `lnbc${params?.amount_sats}n1pjmesh...`, + }, + }) + } + + case 'mesh.send-coordinate': { + console.log(`[Mesh] Send coordinate: ${params?.lat}, ${params?.lng} to contact ${params?.contact_id}`) + return res.json({ + result: { + sent: true, + message_id: Math.floor(Math.random() * 10000) + 300, + lat: Math.round((params?.lat || 0) * 1000000), + lng: Math.round((params?.lng || 0) * 1000000), + }, + }) + } + + case 'mesh.send-alert': { + console.log(`[Mesh] Send alert: ${params?.alert_type} — ${params?.message}`) + return res.json({ + result: { + sent: true, + alert_type: params?.alert_type || 'status', + signed: true, + }, + }) + } + + case 'mesh.outbox': { + return res.json({ + result: { + messages: [ + { + id: 1, + dest_did: 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb', + from_did: 'did:key:z6MkSelf', + created_at: new Date(Date.now() - 1800000).toISOString(), + ttl_secs: 86400, + retry_count: 3, + relay_hops: 0, + expired: false, + }, + { + id: 2, + dest_did: 'did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp7NQD5EjEREWh', + from_did: 'did:key:z6MkSelf', + created_at: new Date(Date.now() - 7200000).toISOString(), + ttl_secs: 86400, + retry_count: 8, + relay_hops: 1, + expired: false, + }, + ], + count: 2, + }, + }) + } + + case 'mesh.session-status': { + const hasSess = (params?.contact_id === 1 || params?.contact_id === 4) + return res.json({ + result: { + has_session: hasSess, + forward_secrecy: hasSess, + message_count: hasSess ? 23 : 0, + ratchet_generation: hasSess ? 7 : 0, + peer_did: hasSess ? 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9' : null, + }, + }) + } + + case 'mesh.rotate-prekeys': { + console.log('[Mesh] Rotating prekeys...') + return res.json({ + result: { + rotated: true, + signed_prekey_id: Math.floor(Math.random() * 1000000), + one_time_prekeys: 10, + }, + }) + } + // ===================================================================== // Transport Layer (unified routing: mesh > lan > tor) // ===================================================================== diff --git a/neode-ui/src/stores/mesh.ts b/neode-ui/src/stores/mesh.ts index a4e4986..8934ba0 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/AppSession.vue b/neode-ui/src/views/AppSession.vue index e1a14d0..2219c83 100644 --- a/neode-ui/src/views/AppSession.vue +++ b/neode-ui/src/views/AppSession.vue @@ -333,7 +333,6 @@ const HTTPS_PROXY_PATHS: Record = { 'immich_server': '/app/immich/', 'tailscale': '/app/tailscale/', 'endurain': '/app/endurain/', - 'indeedhub': '/app/indeedhub/', 'dwn': '/app/dwn/', } @@ -385,6 +384,17 @@ const appUrl = computed(() => { const proxyPath = PROXY_APPS[id] if (proxyPath) return `${window.location.origin}${proxyPath}` + // IndeedHub: always direct port (X-Frame-Options removed by deploy script) + if (id === 'indeedhub') { + const port = APP_PORTS[id] + if (port) { + let base = `${window.location.protocol}//${window.location.hostname}:${port}` + const subpath = route.query.path as string | undefined + if (subpath) base += subpath + return base + } + } + // HTTPS: use nginx proxy to avoid mixed content (browser blocks HTTP iframes in HTTPS pages) if (window.location.protocol === 'https:') { const httpsProxy = HTTPS_PROXY_PATHS[id] diff --git a/neode-ui/src/views/Apps.vue b/neode-ui/src/views/Apps.vue index 15def25..a432063 100644 --- a/neode-ui/src/views/Apps.vue +++ b/neode-ui/src/views/Apps.vue @@ -1,18 +1,50 @@