diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index adda7e00..957c44ce 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -777,7 +777,7 @@ const SEED_MOCKDATA = { } // Helper to build a static app entry -function staticApp({ id, title, version, shortDesc, longDesc, license, state, lanPort, torHost, icon }) { +function staticApp({ id, title, version, shortDesc, longDesc, license, state, lanPort, torHost, icon, noUi }) { return { title, version, @@ -800,8 +800,13 @@ function staticApp({ id, title, version, shortDesc, longDesc, license, state, la 'support-site': '#', 'marketing-site': '#', 'donation-url': null, + // ui:null mirrors the real backend's ui_detection verdict for headless + // services (databases/APIs/workers): address present, no Launch button, + // files under the Services tab. interfaces: { - main: { name: 'Web Interface', description: `${title} web interface`, ui: true }, + main: noUi + ? { name: 'Service', description: `${title} backend service`, ui: null } + : { name: 'Web Interface', description: `${title} web interface`, ui: true }, }, }, installed: { @@ -891,6 +896,28 @@ const staticDevApps = { lanPort: 4403, icon: '/assets/img/app-icons/meshcore.svg', }), + // Self-deployed compose stack (v1.7.117+ services classification demo): + // the main app serves a real web page → launchable; its media backend has + // an exposed port but no UI → Services tab, no Launch button. + podsteadr: staticApp({ + id: 'podsteadr', + title: 'Podsteadr', + version: '0.4.1', + shortDesc: 'Self-hosted podcast studio', + longDesc: 'Record, host, and publish podcasts from your own node — deployed by hand outside the app store to demo third-party stacks.', + state: 'running', + lanPort: 8095, + }), + 'podsteadr-mediamtx': staticApp({ + id: 'podsteadr-mediamtx', + title: 'Podsteadr MediaMTX', + version: '1.9.3', + shortDesc: 'RTMP/WebRTC media backend', + longDesc: 'Streaming backend for Podsteadr. Speaks RTMP and WebRTC on exposed ports — no web page of its own.', + state: 'running', + lanPort: 8889, + noUi: true, + }), filebrowser: staticApp({ id: 'filebrowser', title: 'File Browser', @@ -3030,18 +3057,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, 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...' } }, + { 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, transport: 'meshcore', sender_pubkey: 'demo01abababababababababababababababababababababababababababab', sender_seq: 1, 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, transport: 'meshcore', sender_pubkey: 'demo02abababababababababababababababababababababababababababab', sender_seq: 2, 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, transport: 'reticulum', sender_pubkey: 'demo03abababababababababababababababababababababababababababab', sender_seq: 3, 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, transport: 'fips', sender_pubkey: 'demo04abababababababababababababababababababababababababababab', sender_seq: 4, 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, transport: 'reticulum', sender_pubkey: 'demo05abababababababababababababababababababababababababababab', sender_seq: 5, 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, transport: 'reticulum', sender_pubkey: 'demo06abababababababababababababababababababababababababababab', sender_seq: 6, 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, transport: 'meshcore', sender_pubkey: 'demo07abababababababababababababababababababababababababababab', sender_seq: 7, 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, transport: 'fips', sender_pubkey: 'demo08abababababababababababababababababababababababababababab', sender_seq: 8, 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, transport: 'tor', sender_pubkey: 'demo09abababababababababababababababababababababababababababab', sender_seq: 9, 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, transport: 'meshcore', sender_pubkey: 'demo10abababababababababababababababababababababababababababab', sender_seq: 10, 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, transport: 'reticulum', sender_pubkey: 'demo11abababababababababababababababababababababababababababab', sender_seq: 11, 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, transport: 'fips', sender_pubkey: 'demo12abababababababababababababababababababababababababababab', sender_seq: 12, message_type: 'invoice', typed_payload: { bolt11: 'lnbc500000n1pjmesh...truncated...', amount_sats: 50000, memo: 'Channel opening fee', paid: true, payment_hash: 'a1b2c3d4e5f6...' } }, ] return res.json({ result: { @@ -4243,6 +4270,100 @@ app.post('/rpc/v1', (req, res) => { return res.json({ result: { rejected: true, id: params?.id || '' } }) } + // ── Mesh chat polish + flash flow (v1.7.117/118 demo coverage) ────── + + // Image-modal "Send via" pills: peer 1 is federation-reachable (LoRa + + // FIPS + Tor all offered), everyone else is radio-only. + case 'mesh.transport-advice': { + const federated = params?.contact_id === 1 + return res.json({ + result: { + tier: federated ? 'choose' : 'auto-mesh', + est_seconds: federated ? 4 : 90, + has_tor: federated, + has_fips: federated, + last_transport: federated ? 'fips' : null, + reason: federated + ? 'Peer reachable over the FIPS overlay — radio optional' + : 'Radio-only peer', + size: params?.size || 0, + mesh_auto_max: 24576, + mesh_hard_max: 262144, + }, + }) + } + + case 'mesh.flash-list-firmware': { + const versions = { + meshcore: ['v1.7.4', 'v1.7.2', 'v1.6.9'], + meshtastic: ['2.5.15', '2.5.13', '2.4.2'], + reticulum: ['latest (rnodeconf autoinstall)'], + } + return res.json({ result: { versions: versions[params?.family] || [] } }) + } + case 'mesh.flash-device': { + mockState.flashJob = { + startedAt: Date.now(), + board: params?.board || 'heltec-v3', + family: params?.family || 'meshcore', + path: params?.path || '/dev/ttyUSB0', + cancelled: false, + } + return res.json({ result: { started: true } }) + } + case 'mesh.flash-status': { + const job = mockState.flashJob + if (!job) return res.json({ result: { active: false, done: true, log_tail: [] } }) + const t = (Date.now() - job.startedAt) / 1000 + // Scripted 35s job: download → erase → write → done. + let stage = 'downloading' + let percent = Math.min(100, Math.round((t / 8) * 100)) + if (job.cancelled) stage = 'failed' + else if (t > 30) { stage = 'done'; percent = 100 } + else if (t > 14) { stage = 'writing'; percent = Math.min(100, Math.round(((t - 14) / 16) * 100)) } + else if (t > 8) { stage = 'erasing'; percent = null } + const log = [ + `Fetching ${job.family} firmware for ${job.board}...`, + ...(t > 4 ? ['Downloaded 2.1 MB firmware image'] : []), + ...(t > 8 ? ['esptool erase_flash: chip erase completed'] : []), + ...(t > 14 ? [`Writing at 0x00010000... (${percent || 0} %)`] : []), + ...(stage === 'done' ? ['Hash of data verified.', 'Hard resetting via RTS pin...'] : []), + ...(job.cancelled ? ['ERROR: flash cancelled by user'] : []), + ] + const active = stage !== 'done' && stage !== 'failed' + if (!active) mockState.flashJob = active ? job : job // keep for final poll + return res.json({ + result: { + active, + board: job.board, + family: job.family, + path: job.path, + stage, + percent, + log_tail: log, + done: !active, + error: job.cancelled ? 'Flash cancelled' : null, + }, + }) + } + case 'mesh.flash-cancel': { + if (mockState.flashJob) mockState.flashJob.cancelled = true + return res.json({ result: { cancelled: true } }) + } + + // Chat actions the demo only needs to acknowledge. + case 'mesh.send-reaction': + case 'mesh.send-reply': + case 'mesh.send-read-receipt': + case 'mesh.edit-message': + case 'mesh.delete-message': + case 'mesh.forward-message': + case 'mesh.send-channel': + case 'mesh.refresh': + case 'mesh.reboot-radio': { + return res.json({ result: { ok: true, sent: true } }) + } + default: { console.log(`[RPC] Unknown method: ${method}`) return res.json({