From 9c7ffbb2638e8e8942a3454d7af130be05c73d17 Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 9 Mar 2026 13:03:53 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20enrich=20mock=20backend=20for=20demo=20?= =?UTF-8?q?=E2=80=94=20add=20all=20missing=20RPC=20handlers=20and=20demo?= =?UTF-8?q?=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes "Method not found: identity.create" on demo onboarding. Adds handlers for all identity, nostr, content, network, router, and peer RPC methods so no method-not-found errors occur anywhere in the demo. Expands marketplace from 2 to 12 apps, adds 5 static dashboard apps, randomizes metrics, and populates peer/message data for a richer demo experience. Co-Authored-By: Claude Opus 4.6 --- neode-ui/mock-backend.js | 515 +++++++++++++++++++++++++++++++++------ 1 file changed, 436 insertions(+), 79 deletions(-) diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index 31b2a0ef..8c9a3990 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -541,10 +541,10 @@ async function uninstallPackage(id) { // Mock data const mockData = { 'server-info': { - id: 'archipelago-dev', + id: 'archipelago-demo', version: '0.1.0', - name: 'Archipelago Dev Server', - pubkey: 'mock-pubkey', + name: 'Archipelago', + pubkey: 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', 'status-info': { restarting: false, 'shutting-down': false, @@ -553,11 +553,12 @@ const mockData = { 'update-progress': null, }, 'lan-address': '192.168.1.100', - unread: 0, - 'wifi-ssids': [], - 'zram-enabled': false, + 'tor-address': 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion', + unread: 3, + 'wifi-ssids': ['Home-5G', 'Archipelago-Mesh', 'Neighbors-Open'], + 'zram-enabled': true, }, - 'package-data': {}, // Will be populated from Docker + 'package-data': {}, // Will be populated from Docker + static apps ui: { name: 'Archipelago', 'ack-welcome': '0.1.0', @@ -569,40 +570,33 @@ const mockData = { }, } -// Static dev apps (always shown in My Apps when using mock backend) -const staticDevApps = { - 'lorabell': { - title: 'LoraBell', - version: '1.0.0', - status: 'running', - state: 'running', +// Helper to build a static app entry +function staticApp({ id, title, version, shortDesc, longDesc, license, state, lanPort, torHost, icon }) { + return { + title, + version, + status: state, + state, 'static-files': { - license: 'MIT', - instructions: 'A LoRa based doorbell', - icon: '/assets/img/app-icons/lorabell.png' + license: license || 'MIT', + instructions: shortDesc, + icon: icon || `/assets/img/app-icons/${id}.png`, }, manifest: { - id: 'lorabell', - title: 'LoraBell', - version: '1.0.0', - description: { - short: 'A LoRa based doorbell', - long: 'A LoRa based doorbell - receive doorbell notifications over LoRa radio.' - }, - 'release-notes': 'Initial release', - license: 'MIT', + id, + title, + version, + description: { short: shortDesc, long: longDesc || shortDesc }, + 'release-notes': 'Latest stable release', + license: license || 'MIT', 'wrapper-repo': '#', 'upstream-repo': '#', 'support-site': '#', 'marketing-site': '#', 'donation-url': null, interfaces: { - main: { - name: 'Web Interface', - description: 'LoraBell web interface', - ui: true - } - } + main: { name: 'Web Interface', description: `${title} web interface`, ui: true }, + }, }, installed: { 'current-dependents': {}, @@ -610,15 +604,65 @@ const staticDevApps = { 'last-backup': null, 'interface-addresses': { main: { - 'tor-address': 'lorabell.onion', - 'lan-address': 'http://192.168.1.166' - } + 'tor-address': torHost ? `${torHost}.onion` : `${id}.onion`, + 'lan-address': lanPort ? `http://192.168.1.100:${lanPort}` : '', + }, }, - status: 'running' - } + status: state, + }, } } +// Static dev apps (always shown in My Apps when using mock backend) +const staticDevApps = { + bitcoin: staticApp({ + id: 'bitcoin', + title: 'Bitcoin Core', + version: '27.1', + shortDesc: 'Full Bitcoin node', + longDesc: 'Validate every transaction and block. Full consensus enforcement — the bedrock of sovereignty.', + state: 'running', + lanPort: 8332, + }), + lnd: staticApp({ + id: 'lnd', + title: 'LND', + version: '0.18.3', + shortDesc: 'Lightning Network Daemon', + longDesc: 'Instant Bitcoin payments with near-zero fees. Open channels, route payments, earn sats.', + state: 'running', + lanPort: 8080, + }), + electrs: staticApp({ + id: 'electrs', + title: 'Electrs', + version: '0.10.6', + shortDesc: 'Electrum Server in Rust', + longDesc: 'Private blockchain indexing for wallet lookups. Connect Sparrow, BlueWallet, or any Electrum-compatible wallet.', + state: 'running', + lanPort: 50001, + }), + mempool: staticApp({ + id: 'mempool', + title: 'Mempool', + version: '3.0.0', + shortDesc: 'Blockchain explorer & fee estimator', + longDesc: 'Real-time mempool visualization, transaction tracking, and fee estimation — all on your own node.', + license: 'AGPL-3.0', + state: 'running', + lanPort: 4080, + }), + lorabell: staticApp({ + id: 'lorabell', + title: 'LoraBell', + version: '1.0.0', + shortDesc: 'LoRa doorbell', + longDesc: 'Receive doorbell notifications over LoRa radio — no WiFi or internet required.', + state: 'running', + lanPort: null, + }), +} + function mergePackageData(dockerApps) { return { ...dockerApps, ...staticDevApps } } @@ -795,11 +839,12 @@ app.post('/rpc/v1', (req, res) => { } case 'server.metrics': { + // Slightly randomize so the dashboard feels alive return res.json({ result: { - cpu: 45.2, - memory: 62.8, - disk: 38.1, + cpu: +(12 + Math.random() * 18).toFixed(1), + memory: +(58 + Math.random() * 8).toFixed(1), + disk: +(34 + Math.random() * 3).toFixed(1), }, }) } @@ -809,23 +854,125 @@ app.post('/rpc/v1', (req, res) => { { id: 'bitcoin', title: 'Bitcoin Core', - description: 'A full Bitcoin node.', - version: '25.0.0', - icon: '/assets/img/bitcoin.svg', - author: 'Bitcoin Core Team', + description: 'Full Bitcoin node — validate transactions, enforce consensus rules, and support the network. The foundation of sovereignty.', + version: '27.1', + icon: '/assets/img/app-icons/bitcoin.png', + author: 'Bitcoin Core', license: 'MIT', + category: 'Bitcoin', }, { - id: 'lightning', - title: 'Core Lightning', - description: 'Lightning Network implementation.', - version: '23.08', - icon: '/assets/img/c-lightning.png', - author: 'Blockstream', + id: 'lnd', + title: 'LND', + description: 'Lightning Network Daemon — instant, low-fee Bitcoin payments. Open channels, route payments, earn routing fees.', + version: '0.18.3', + icon: '/assets/img/app-icons/lnd.png', + author: 'Lightning Labs', license: 'MIT', + category: 'Bitcoin', + }, + { + id: 'electrs', + title: 'Electrs', + description: 'Electrum Server in Rust — index the blockchain for fast wallet lookups. Connect your hardware wallets privately.', + version: '0.10.6', + icon: '/assets/img/app-icons/electrs.png', + author: 'Roman Zeyde', + license: 'MIT', + category: 'Bitcoin', + }, + { + id: 'mempool', + title: 'Mempool', + description: 'Bitcoin blockchain explorer and mempool visualizer. Monitor transactions, fees, and network activity in real time.', + version: '3.0.0', + icon: '/assets/img/app-icons/mempool.png', + author: 'Mempool Space', + license: 'AGPL-3.0', + category: 'Bitcoin', + }, + { + id: 'btcpay', + title: 'BTCPay Server', + description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments in your store — no fees, no middlemen, no KYC.', + version: '2.0.4', + icon: '/assets/img/app-icons/btcpay.png', + author: 'BTCPay Server', + license: 'MIT', + category: 'Commerce', + }, + { + id: 'fedimint', + title: 'Fedimint', + description: 'Federated Chaumian e-cash mint — community custody, private payments, and Lightning gateways for your tribe.', + version: '0.4.3', + icon: '/assets/img/app-icons/fedimint.png', + author: 'Fedimint', + license: 'MIT', + category: 'Bitcoin', + }, + { + id: 'vaultwarden', + title: 'Vaultwarden', + description: 'Self-hosted password manager compatible with Bitwarden clients. Keep your credentials under your own roof.', + version: '1.32.5', + icon: '/assets/img/app-icons/vaultwarden.png', + author: 'Vaultwarden', + license: 'AGPL-3.0', + category: 'Privacy', + }, + { + id: 'nextcloud', + title: 'Nextcloud', + description: 'Your personal cloud — files, calendar, contacts, and collaboration. Replace Google Drive and Dropbox entirely.', + version: '29.0.0', + icon: '/assets/img/app-icons/nextcloud.png', + author: 'Nextcloud GmbH', + license: 'AGPL-3.0', + category: 'Cloud', + }, + { + id: 'nostr-relay', + title: 'Nostr Relay', + description: 'Run your own Nostr relay — sovereign social networking. Publish notes, follow friends, no censorship possible.', + version: '0.34.0', + icon: '/assets/img/app-icons/nostr-relay.png', + author: 'nostr-rs-relay', + license: 'MIT', + category: 'Social', + }, + { + id: 'home-assistant', + title: 'Home Assistant', + description: 'Open-source home automation — control lights, locks, cameras, and sensors. Smart home without the cloud dependency.', + version: '2024.12.0', + icon: '/assets/img/app-icons/home-assistant.png', + author: 'Home Assistant', + license: 'Apache-2.0', + category: 'IoT', + }, + { + id: 'syncthing', + title: 'Syncthing', + description: 'Continuous peer-to-peer file synchronization. Sync folders across devices without any cloud service.', + version: '1.28.1', + icon: '/assets/img/app-icons/syncthing.png', + author: 'Syncthing Foundation', + license: 'MPL-2.0', + category: 'Cloud', + }, + { + id: 'tor', + title: 'Tor', + description: 'Anonymous communication — route traffic through the Tor network. Access your node from anywhere, privately.', + version: '0.4.8.13', + icon: '/assets/img/app-icons/tor.png', + author: 'Tor Project', + license: 'BSD-3', + category: 'Privacy', }, ] - + return res.json({ result: mockApps }) } @@ -893,10 +1040,179 @@ app.post('/rpc/v1', (req, res) => { return res.json({ result: { success: true } }) } - case 'node-messages-received': - case 'node.messages': - case 'node.notifications': + // ========================================================================= + // Identity & Onboarding + // ========================================================================= + case 'identity.create': { + const { name, purpose } = params || {} + console.log(`[Identity] Created identity: "${name || 'Personal'}" (${purpose || 'personal'})`) + return res.json({ result: { success: true, id: `identity-${Date.now()}` } }) + } + + case 'identity.register-name': { + const { name } = params || {} + console.log(`[Identity] Registered name: ${name}`) + return res.json({ result: { success: true, id: `name-${Date.now()}` } }) + } + + case 'identity.remove-name': { + return res.json({ result: { success: true } }) + } + + case 'identity.set-default': { + return res.json({ result: { success: true } }) + } + + case 'identity.delete': { + return res.json({ result: { success: true } }) + } + + case 'identity.issue-credential': { + return res.json({ result: { success: true, credential_id: `cred-${Date.now()}` } }) + } + + case 'identity.revoke-credential': { + return res.json({ result: { success: true } }) + } + + // ========================================================================= + // Nostr + // ========================================================================= + case 'nostr.add-relay': { + const { url } = params || {} + console.log(`[Nostr] Added relay: ${url}`) + return res.json({ result: { success: true } }) + } + + case 'nostr.remove-relay': { + return res.json({ result: { success: true } }) + } + + case 'nostr.toggle-relay': { + return res.json({ result: { success: true } }) + } + + // ========================================================================= + // Content & Network + // ========================================================================= + case 'content.remove': { + return res.json({ result: { success: true } }) + } + + case 'content.set-pricing': { + return res.json({ result: { success: true } }) + } + + case 'network.accept-request': { + return res.json({ result: { success: true } }) + } + + case 'network.reject-request': { + return res.json({ result: { success: true } }) + } + + // ========================================================================= + // Server & Auth extras + // ========================================================================= + case 'server.health': { + return res.json({ + result: { + status: 'healthy', + uptime: Math.floor(process.uptime()), + services: { + backend: 'running', + nginx: 'running', + podman: 'running', + tor: 'running', + }, + }, + }) + } + + case 'auth.changePassword': { + const { currentPassword } = params || {} + if (currentPassword !== userState.passwordHash && currentPassword !== MOCK_PASSWORD) { + return res.json({ error: { code: -32603, message: 'Current password is incorrect' } }) + } + userState.passwordHash = params.newPassword + console.log('[Auth] Password changed') + return res.json({ result: { success: true } }) + } + + // ========================================================================= + // Router (port forwarding) + // ========================================================================= + case 'router.add-forward': { + console.log(`[Router] Added forward: ${JSON.stringify(params)}`) + return res.json({ result: { success: true, id: `fwd-${Date.now()}` } }) + } + + case 'router.remove-forward': { + return res.json({ result: { success: true } }) + } + + // ========================================================================= + // Tor & Peer Networking + // ========================================================================= + case 'node.tor-address': { + return res.json({ + result: { + tor_address: 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion', + }, + }) + } + case 'node-list-peers': { + return res.json({ + result: { + peers: [ + { onion: 'peer1abc2def3ghi4jkl5mno6pqr7stu8vwx9yz.onion', pubkey: 'a1b2c3d4e5f6', name: 'satoshi-node' }, + { onion: 'peer2xyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba.onion', pubkey: 'f6e5d4c3b2a1', name: 'lightning-lab' }, + { onion: 'peer3mno6pqr7stu8vwx9yzabc2def3ghi4jkl5.onion', pubkey: 'c3d4e5f6a1b2', name: 'sovereign-relay' }, + ], + }, + }) + } + + case 'node-check-peer': { + return res.json({ result: { onion: params?.onion || '', reachable: Math.random() > 0.2 } }) + } + + case 'node-add-peer': { + console.log(`[Peers] Added peer: ${params?.onion}`) + return res.json({ result: { success: true } }) + } + + case 'node-send-message': { + console.log(`[Peers] Sent message to: ${params?.onion}`) + return res.json({ result: { ok: true, sent_to: params?.onion || '' } }) + } + + case 'node-nostr-discover': { + return res.json({ + result: { + nodes: [ + { did: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9', onion: 'disc1abc2def3ghi4jkl5mno6pqr7stu8vwx9yz.onion', pubkey: 'disc1pub', node_address: '192.168.1.50' }, + { did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', onion: 'disc2xyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba.onion', pubkey: 'disc2pub', node_address: '192.168.1.51' }, + ], + }, + }) + } + + case 'node-messages-received': + case 'node.messages': { + return res.json({ + result: { + messages: [ + { from_pubkey: 'a1b2c3d4e5f6', message: 'Hey, your relay is online! Nice uptime.', timestamp: new Date(Date.now() - 3600000).toISOString() }, + { from_pubkey: 'f6e5d4c3b2a1', message: 'Channel opened successfully. 500k sats capacity.', timestamp: new Date(Date.now() - 7200000).toISOString() }, + { from_pubkey: 'c3d4e5f6a1b2', message: 'Backup sync complete. All good on my end.', timestamp: new Date(Date.now() - 86400000).toISOString() }, + ], + }, + }) + } + + case 'node.notifications': { return res.json({ result: [] }) } @@ -1018,10 +1334,10 @@ app.get('/app/filebrowser/api/raw/*', (req, res) => { }) // Claude API Proxy (reads ANTHROPIC_API_KEY from environment) +// Uses fetch (Node 22+) for reliable DNS resolution and streaming in Docker/Alpine // ============================================================================= -import https from 'https' -app.post('/aiui/api/claude/*', (req, res) => { +app.post('/aiui/api/claude/*', async (req, res) => { const apiKey = process.env.ANTHROPIC_API_KEY if (!apiKey) { return res.status(500).json({ @@ -1053,38 +1369,63 @@ app.post('/aiui/api/claude/*', (req, res) => { } const bodyStr = JSON.stringify(body) + const url = `https://api.anthropic.com${apiPath}` + console.log(`[Claude Proxy] → POST ${url} (${bodyStr.length} bytes, model: ${body?.model || 'unknown'})`) - const options = { - hostname: 'api.anthropic.com', - port: 443, - path: apiPath, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': apiKey, - 'anthropic-version': '2023-06-01', - 'Content-Length': Buffer.byteLength(bodyStr), - }, - } + try { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 60000) - const proxyReq = https.request(options, (proxyRes) => { - res.writeHead(proxyRes.statusCode, proxyRes.headers) - proxyRes.pipe(res) - }) + const apiRes = await fetch(url, { + method: 'POST', + signal: controller.signal, + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + }, + body: bodyStr, + }) - proxyReq.on('error', (err) => { - const msg = err.message || err.code || JSON.stringify(err) || 'Unknown proxy error' - console.error('[Claude Proxy] Error:', msg) + clearTimeout(timeout) + console.log(`[Claude Proxy] ← ${apiRes.status}`) + + // Forward status and headers + res.status(apiRes.status) + for (const [key, value] of apiRes.headers.entries()) { + // Skip hop-by-hop headers + if (!['transfer-encoding', 'connection', 'keep-alive'].includes(key.toLowerCase())) { + res.setHeader(key, value) + } + } + + // Stream the response body + if (apiRes.body) { + const reader = apiRes.body.getReader() + const pump = async () => { + while (true) { + const { done, value } = await reader.read() + if (done) { res.end(); return } + if (!res.writableEnded) res.write(value) + } + } + pump().catch((err) => { + console.error('[Claude Proxy] Stream error:', err.message) + if (!res.writableEnded) res.end() + }) + } else { + res.end() + } + } catch (err) { + const msg = err.name === 'AbortError' ? 'Request timed out (60s)' : (err.message || 'Unknown error') + console.error(`[Claude Proxy] Error: ${msg}`) if (!res.headersSent) { res.status(502).json({ type: 'error', error: { type: 'proxy_error', message: msg } }) } - }) - - proxyReq.write(bodyStr) - proxyReq.end() + } }) // Ollama (local AI) proxy — forwards to localhost:11434 @@ -1273,10 +1614,26 @@ server.listen(PORT, '0.0.0.0', async () => { ║ ║ ║ Container Runtime: ${runtime.available ? `✅ ${runtime.runtime}`.padEnd(40) : '❌ Not available'.padEnd(40)}║ ║ Docker API: ✅ Connected ║ +║ Claude API Key: ${process.env.ANTHROPIC_API_KEY ? '✅ Set (' + process.env.ANTHROPIC_API_KEY.slice(0, 12) + '...)' : '❌ Not set (chat disabled)'.padEnd(40)}║ ║ ║ ╚════════════════════════════════════════════════════════════╝ `) console.log('Mock backend is running. Press Ctrl+C to stop.\n') + + // Pre-check Anthropic API connectivity + if (process.env.ANTHROPIC_API_KEY) { + try { + const dns = await import('dns') + dns.lookup('api.anthropic.com', (err, address) => { + if (err) { + console.error('[Claude Proxy] ⚠ DNS lookup failed for api.anthropic.com:', err.message) + console.error('[Claude Proxy] Chat will fail. Check container DNS settings.') + } else { + console.log('[Claude Proxy] ✅ api.anthropic.com resolves to', address) + } + }) + } catch { /* ignore */ } + } // Periodically update package data from Docker (merge with static dev apps) // Only poll if container runtime is available (avoids log spam in demo/Docker deployments)