diff --git a/docker/lnd-ui/index.html b/docker/lnd-ui/index.html index 0df9a22c..1ef7df93 100644 --- a/docker/lnd-ui/index.html +++ b/docker/lnd-ui/index.html @@ -196,6 +196,20 @@

0

+ +
@@ -500,6 +514,12 @@ if (el) el.textContent = text; } + function formatSats(sats) { + if (sats >= 1_000_000) return (sats / 1_000_000).toFixed(2).replace(/\.00$/, '') + 'M sats'; + if (sats >= 1_000) return (sats / 1_000).toFixed(1).replace(/\.0$/, '') + 'k sats'; + return sats.toLocaleString() + ' sats'; + } + async function loadLogs() { const logsContent = document.getElementById('logsContent'); const backendUrl = getBackendUrl(); @@ -532,6 +552,29 @@ data.channelCount = (ch.channels && ch.channels.length) || 0; } } catch (_) {} + // Balances — cards stay hidden when the endpoints are unavailable. + try { + const wRes = await fetch(backendUrl + '/proxy/lnd/v1/balance/blockchain', { credentials: 'include' }); + if (wRes.ok) { + const w = await wRes.json(); + const sats = Number(w.confirmed_balance ?? w.total_balance ?? 0); + if (!isNaN(sats)) { + setText('onchainBalance', formatSats(sats)); + document.getElementById('onchainBalanceCard').style.display = ''; + } + } + } catch (_) {} + try { + const cRes = await fetch(backendUrl + '/proxy/lnd/v1/balance/channels', { credentials: 'include' }); + if (cRes.ok) { + const c = await cRes.json(); + const sats = Number((c.local_balance && c.local_balance.sat) ?? c.balance ?? 0); + if (!isNaN(sats)) { + setText('channelBalance', formatSats(sats)); + document.getElementById('channelBalanceCard').style.display = ''; + } + } + } catch (_) {} data.grpcReachable = data.restReachable; applyLiveData(data); } diff --git a/docs/demo-build-info.md b/docs/demo-build-info.md index ce73e129..4992b7c9 100644 --- a/docs/demo-build-info.md +++ b/docs/demo-build-info.md @@ -1,9 +1,9 @@ # Archipelago Public Demo — build info & status -**Status:** implemented & deployable (2026-06-22) -**Branch:** `demo-build` (worktree `../archy-demo-build`), pushed to +**Status:** implemented & deployable (2026-07-14) +**Branch:** `main` — the demo machinery was merged from the old `demo-build` +branch and now lives on main, pushed to `gitea-vps2` = `http://146.59.87.168:3000/lfg2025/archy.git`. -**Main/prod is untouched** — all demo work lives only on `demo-build`. A public, click-to-play demo of the Archipelago UI, 100% mock-data driven, multi-visitor, deployed via Portainer. See also `docs/archive/demo-deployment-design.md` @@ -18,7 +18,7 @@ Build-from-repo (works today, no registry needed): | Field | Value | |-------|-------| | Repository URL | `http://146.59.87.168:3000/lfg2025/archy.git` | -| Reference | `refs/heads/demo-build` | +| Reference | `refs/heads/main` | | Compose path | `docker-compose.demo.yml` | | Auth | user `lfg2025`, password = Gitea token | | UI port | **2100** · Login password: **`entertoexit`** | diff --git a/neode-ui/Dockerfile.backend b/neode-ui/Dockerfile.backend index f9ed0339..22ed353c 100644 --- a/neode-ui/Dockerfile.backend +++ b/neode-ui/Dockerfile.backend @@ -15,12 +15,14 @@ RUN npm install COPY neode-ui/ ./ # Sibling assets the mock backend reads relative to /app (../docker, ../demo): -# the Bitcoin UI mock shell and any curated cloud files dropped into demo/files. +# the Bitcoin UI mock shell and the curated cloud files (demo/files drop-ins + +# the committed demo/content library — both are scanned by loadDemoDiskFiles). COPY docker/bitcoin-ui /docker/bitcoin-ui COPY docker/electrs-ui /docker/electrs-ui COPY docker/lnd-ui /docker/lnd-ui COPY docker/fedimint-ui /docker/fedimint-ui COPY demo/files /demo/files +COPY demo/content /demo/content # Expose port EXPOSE 5959 diff --git a/neode-ui/docker/nginx-demo.conf b/neode-ui/docker/nginx-demo.conf index 01bfa11e..123dd4eb 100644 --- a/neode-ui/docker/nginx-demo.conf +++ b/neode-ui/docker/nginx-demo.conf @@ -17,6 +17,12 @@ http { # Allow large uploads globally (filebrowser, etc.) client_max_body_size 0; + # WebSocket upgrade passthrough (mempool live data, etc.) + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + server { listen 80 default_server; server_name _; @@ -116,12 +122,15 @@ http { sub_filter 'url(/' 'url(/app/indeedhub/'; } - # Mempool: same approach. NOTE mempool.space is a strict third-party app — - # its data/websocket calls may still be blocked; iframe is best-effort. + # Mempool: same approach. mempool.guide (not mempool.space, which blocks + # proxied embedding) — strip framing headers and rewrite absolute paths. location /app/mempool/ { - proxy_pass https://mempool.space/; + proxy_pass https://mempool.guide/; proxy_http_version 1.1; - proxy_set_header Host mempool.space; + proxy_set_header Host mempool.guide; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 3600; proxy_set_header Accept-Encoding ""; proxy_ssl_server_name on; proxy_hide_header X-Frame-Options; diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index 2831631a..75b4ae66 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -233,6 +233,14 @@ function seedMockState() { { federation_id: 'fed1' + randomHex(28), name: 'Archipelago Federation', balance_sats: 180_000 }, { federation_id: 'fed1' + randomHex(28), name: 'Bitcoin Park Mint', balance_sats: 42_500 }, ], + // Networking-profits pricing (mirrors crate::streaming::pricing defaults, + // with a couple enabled so the demo page looks alive). + streamingServices: [ + { service_id: 'content-download', name: 'Content Downloads', metric: 'bytes', step_size: 1_048_576, price_per_step: 1, min_steps: 0, enabled: true, description: 'Pay-per-byte content downloads from this node', accepted_mints: [] }, + { service_id: 'federation-sync', name: 'Federation Sync Access', metric: 'milliseconds', step_size: 60_000, price_per_step: 1, min_steps: 5, enabled: false, description: 'Timed access to federation sync endpoint', accepted_mints: [] }, + { service_id: 'api-access', name: 'API Access', metric: 'requests', step_size: 1, price_per_step: 1, min_steps: 10, enabled: false, description: 'Per-request API access for external consumers', accepted_mints: [] }, + { service_id: 'nostr-relay', name: 'Nostr Relay Access', metric: 'milliseconds', step_size: 3_600_000, price_per_step: 10, min_steps: 1, enabled: true, description: 'Timed access to the local Nostr relay', accepted_mints: [] }, + ], } } @@ -333,7 +341,7 @@ async function getDockerContainers() { const appMetadata = { 'bitcoin': { title: 'Bitcoin Core', - icon: '/assets/img/app-icons/bitcoin.svg', + icon: '/assets/img/app-icons/bitcoin-core.png', description: 'Full Bitcoin node implementation' }, 'btcpay-server': { @@ -353,7 +361,7 @@ async function getDockerContainers() { }, 'endurain': { title: 'Endurain', - icon: '/assets/img/endurain.png', + icon: '/assets/img/app-icons/endurain.png', description: 'Application platform' }, 'fedimint': { @@ -363,22 +371,22 @@ async function getDockerContainers() { }, 'morphos-server': { title: 'MorphOS Server', - icon: '/assets/img/morphos.png', + icon: '/assets/img/app-icons/morphos.png', description: 'Server platform' }, 'lightning-stack': { title: 'Lightning Stack', - icon: '/assets/img/app-icons/lightning-stack.png', + icon: '/assets/img/app-icons/lnd.png', description: 'Lightning Network (LND)' }, 'mempool': { title: 'Mempool', - icon: '/assets/img/app-icons/mempool.png', + icon: '/assets/img/app-icons/mempool.webp', description: 'Bitcoin blockchain explorer' }, 'mempool-electrs': { title: 'Electrs', - icon: '/assets/img/app-icons/electrs.svg', + icon: '/assets/img/app-icons/electrumx.webp', description: 'Electrum protocol indexer for Bitcoin' }, 'ollama': { @@ -393,7 +401,7 @@ async function getDockerContainers() { }, 'penpot': { title: 'Penpot', - icon: '/assets/img/penpot.webp', + icon: '/assets/img/app-icons/archipelago-a.svg', description: 'Open-source design and prototyping' } } @@ -480,15 +488,15 @@ async function isContainerRuntimeAvailable() { // Marketplace metadata lookup for install (title, description, icon, version) const marketplaceMetadata = { 'bitcoin-knots': { title: 'Bitcoin Knots', shortDesc: 'Full Bitcoin node — validate and relay blocks and transactions', icon: '/assets/img/app-icons/bitcoin-knots.webp' }, - 'electrs': { title: 'Electrs', shortDesc: 'Electrum protocol indexer for Bitcoin', icon: '/assets/img/app-icons/electrs.svg' }, + 'electrs': { title: 'Electrs', shortDesc: 'Electrum protocol indexer for Bitcoin', icon: '/assets/img/app-icons/electrumx.webp' }, 'btcpay-server': { title: 'BTCPay Server', shortDesc: 'Self-hosted Bitcoin payment processor', icon: '/assets/img/app-icons/btcpay-server.png' }, - 'lnd': { title: 'LND', shortDesc: 'Lightning Network Daemon', icon: '/assets/img/app-icons/lnd.svg' }, + 'lnd': { title: 'LND', shortDesc: 'Lightning Network Daemon', icon: '/assets/img/app-icons/lnd.png' }, 'mempool': { title: 'Mempool Explorer', shortDesc: 'Bitcoin blockchain and mempool visualizer', icon: '/assets/img/app-icons/mempool.webp' }, 'homeassistant': { title: 'Home Assistant', shortDesc: 'Open-source home automation platform', icon: '/assets/img/app-icons/homeassistant.png' }, 'grafana': { title: 'Grafana', shortDesc: 'Analytics and monitoring dashboards', icon: '/assets/img/app-icons/grafana.png' }, 'searxng': { title: 'SearXNG', shortDesc: 'Privacy-respecting metasearch engine', icon: '/assets/img/app-icons/searxng.png' }, 'ollama': { title: 'Ollama', shortDesc: 'Run large language models locally', icon: '/assets/img/app-icons/ollama.png' }, - 'penpot': { title: 'Penpot', shortDesc: 'Open-source design and prototyping platform', icon: '/assets/img/app-icons/penpot.webp' }, + 'penpot': { title: 'Penpot', shortDesc: 'Open-source design and prototyping platform', icon: '/assets/img/app-icons/archipelago-a.svg' }, 'nextcloud': { title: 'Nextcloud', shortDesc: 'Self-hosted cloud storage and collaboration', icon: '/assets/img/app-icons/nextcloud.webp' }, 'vaultwarden': { title: 'Vaultwarden', shortDesc: 'Self-hosted password manager (Bitwarden-compatible)', icon: '/assets/img/app-icons/vaultwarden.webp' }, 'jellyfin': { title: 'Jellyfin', shortDesc: 'Free media server for movies, music, and photos', icon: '/assets/img/app-icons/jellyfin.webp' }, @@ -502,10 +510,10 @@ const marketplaceMetadata = { 'fedimint': { title: 'Fedimint', shortDesc: 'Federated Bitcoin mint with Guardian UI', icon: '/assets/img/app-icons/fedimint.png' }, 'indeedhub': { title: 'Indeehub', shortDesc: 'Bitcoin documentary streaming platform', icon: '/assets/img/app-icons/indeedhub.png' }, 'dwn': { title: 'Decentralized Web Node', shortDesc: 'Store and sync personal data with DID-based access', icon: '/assets/img/app-icons/dwn.svg' }, - 'nostr-rs-relay': { title: 'Nostr Relay', shortDesc: 'Run your own Nostr relay', icon: '/assets/img/app-icons/nostr-rs-relay.svg' }, - 'syncthing': { title: 'Syncthing', shortDesc: 'Peer-to-peer file synchronization', icon: '/assets/img/app-icons/syncthing.png' }, + 'nostr-rs-relay': { title: 'Nostr Relay', shortDesc: 'Run your own Nostr relay', icon: '/assets/img/app-icons/nostrudel.svg' }, + 'syncthing': { title: 'Syncthing', shortDesc: 'Peer-to-peer file synchronization', icon: '/assets/img/app-icons/archipelago-a.svg' }, 'thunderhub': { title: 'ThunderHub', shortDesc: 'Lightning node management UI with channel management and payments', icon: '/assets/img/app-icons/thunderhub.svg' }, - 'tor': { title: 'Tor', shortDesc: 'Anonymous communication over the Tor network', icon: '/assets/img/app-icons/tor.png' }, + 'tor': { title: 'Tor', shortDesc: 'Anonymous communication over the Tor network', icon: '/assets/img/app-icons/tor.svg' }, 'amin': { title: 'Amin', shortDesc: 'Administrative interface for Archipelago', icon: '/assets/icon/pwa-192x192-v2.png' }, } @@ -720,6 +728,9 @@ const SEED_MOCKDATA = { updated: false, 'backup-progress': null, 'update-progress': null, + // The mock has no container scanner — report the scan as done so app + // cards never sit on "Checking..." in demo/dev. + 'containers-scanned': true, }, 'lan-address': 'localhost', 'tor-address': 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion', @@ -812,7 +823,7 @@ const staticDevApps = { longDesc: 'Instant Bitcoin payments with near-zero fees. Open channels, route payments, earn sats.', state: 'running', lanPort: 8080, - icon: '/assets/img/app-icons/lnd.svg', + icon: '/assets/img/app-icons/lnd.png', }), electrumx: staticApp({ id: 'electrumx', @@ -872,6 +883,92 @@ const staticDevApps = { longDesc: 'Federated Chaumian e-cash mint with Guardian UI. Community custody, private payments, and Lightning gateways.', state: 'running', lanPort: 8175, + icon: '/assets/img/app-icons/fedimint.png', + }), + 'btcpay-server': staticApp({ + id: 'btcpay-server', + title: 'BTCPay Server', + version: '2.0.4', + shortDesc: 'Self-hosted Bitcoin payment processor', + longDesc: 'Accept Bitcoin payments in your store — no fees, no middlemen, no KYC.', + state: 'running', + lanPort: 23000, + icon: '/assets/img/app-icons/btcpay-server.png', + }), + grafana: staticApp({ + id: 'grafana', + title: 'Grafana', + version: '11.4.0', + shortDesc: 'Analytics and monitoring dashboards', + longDesc: 'Visualize node metrics, Bitcoin sync progress, and Lightning routing on custom dashboards.', + license: 'AGPL-3.0', + state: 'running', + lanPort: 3000, + icon: '/assets/img/app-icons/grafana.png', + }), + nextcloud: staticApp({ + id: 'nextcloud', + title: 'Nextcloud', + version: '29.0.0', + shortDesc: 'Self-hosted cloud storage and collaboration', + longDesc: 'Files, calendar, contacts, and collaboration — replace Google Drive and Dropbox entirely.', + license: 'AGPL-3.0', + state: 'running', + lanPort: 8082, + icon: '/assets/img/app-icons/nextcloud.webp', + }), + jellyfin: staticApp({ + id: 'jellyfin', + title: 'Jellyfin', + version: '10.10.3', + shortDesc: 'Free media server for movies, music, and photos', + longDesc: 'Stream your media collection to any device — no subscription, no tracking.', + license: 'GPL-2.0', + state: 'running', + lanPort: 8096, + icon: '/assets/img/app-icons/jellyfin.webp', + }), + vaultwarden: staticApp({ + id: 'vaultwarden', + title: 'Vaultwarden', + version: '1.32.5', + shortDesc: 'Self-hosted password manager', + longDesc: 'Bitwarden-compatible password manager. Keep your credentials under your own roof.', + license: 'AGPL-3.0', + state: 'running', + lanPort: 8222, + icon: '/assets/img/app-icons/vaultwarden.webp', + }), + 'nostr-rs-relay': staticApp({ + id: 'nostr-rs-relay', + title: 'Nostr Relay', + version: '0.9.0', + shortDesc: 'Run your own Nostr relay', + longDesc: 'Sovereign social networking — publish notes and serve your community, censorship-free.', + state: 'running', + lanPort: 7000, + icon: '/assets/img/app-icons/nostrudel.svg', + }), + searxng: staticApp({ + id: 'searxng', + title: 'SearXNG', + version: '2024.12.16', + shortDesc: 'Privacy-respecting metasearch engine', + longDesc: 'Aggregate search results from dozens of engines with zero tracking.', + license: 'AGPL-3.0', + state: 'running', + lanPort: 8888, + icon: '/assets/img/app-icons/searxng.png', + }), + 'uptime-kuma': staticApp({ + id: 'uptime-kuma', + title: 'Uptime Kuma', + version: '1.23.16', + shortDesc: 'Self-hosted monitoring tool', + longDesc: 'Monitor uptime for your services with alerts and status pages.', + state: 'running', + lanPort: 3001, + icon: '/assets/img/app-icons/uptime-kuma.webp', }), } @@ -969,6 +1066,47 @@ function mockBitcoinNetworkInfo() { } } +// One monitoring MetricSnapshot in the exact shape Monitoring.vue expects: +// epoch-second timestamps, nested system metrics, per-container stats with +// limits + block IO, rpc_latency_ms and ws_connections at the top level. +function monitoringSnapshot(tsSecs, seed = 0) { + const wave = Math.sin((tsSecs / 60 + seed) / 6) + const mkContainer = (name, cpuPct, mem, limit) => ({ + name, + cpu_percent: cpuPct, + mem_used_bytes: mem, + mem_limit_bytes: limit, + net_rx_bytes: Math.floor(Math.random() * 4_000_000), + net_tx_bytes: Math.floor(Math.random() * 2_500_000), + block_read_bytes: Math.floor(Math.random() * 9_000_000), + block_write_bytes: Math.floor(Math.random() * 5_000_000), + }) + return { + timestamp: tsSecs, + system: { + cpu_percent: +(14 + wave * 6 + Math.random() * 8).toFixed(1), + mem_used_bytes: Math.floor(6_000_000_000 + wave * 600_000_000 + Math.random() * 300_000_000), + mem_total_bytes: 16_000_000_000, + disk_used_bytes: 620_000_000_000, + disk_total_bytes: 1_800_000_000_000, + net_rx_bytes: Math.floor(2_000_000 + Math.random() * 6_000_000), + net_tx_bytes: Math.floor(1_000_000 + Math.random() * 4_000_000), + load_avg_1: +(0.5 + Math.random() * 1.2).toFixed(2), + load_avg_5: +(0.7 + Math.random() * 0.9).toFixed(2), + load_avg_15: +(0.6 + Math.random() * 0.7).toFixed(2), + }, + containers: [ + mkContainer('bitcoin-knots', +(6 + Math.random() * 5).toFixed(1), 1_200_000_000, 4_000_000_000), + mkContainer('lnd', +(2 + Math.random() * 3).toFixed(1), 480_000_000, 2_000_000_000), + mkContainer('electrs', +(9 + Math.random() * 7).toFixed(1), 890_000_000, 2_000_000_000), + mkContainer('mempool', +(3 + Math.random() * 4).toFixed(1), 320_000_000, 1_000_000_000), + mkContainer('filebrowser', +(0.4 + Math.random()).toFixed(1), 45_000_000, 512_000_000), + ], + rpc_latency_ms: +(2 + Math.random() * 9).toFixed(1), + ws_connections: 2, + } +} + function bitcoinRelayStatusPayload() { return { settings: bitcoinRelayMockState.settings, @@ -1155,8 +1293,23 @@ function lndChannels() { } app.get('/proxy/lnd/v1/getinfo', (_req, res) => res.json(lndGetinfo())) app.get('/proxy/lnd/v1/channels', (_req, res) => res.json(lndChannels())) +// Balances polled by the lnd-ui shell's stat cards (LND REST shapes). +app.get('/proxy/lnd/v1/balance/blockchain', (_req, res) => res.json({ + total_balance: '2450000', confirmed_balance: '2350000', unconfirmed_balance: '100000', +})) +app.get('/proxy/lnd/v1/balance/channels', (_req, res) => res.json({ + balance: '8250000', + local_balance: { sat: '8250000', msat: '8250000000' }, + remote_balance: { sat: '11750000', msat: '11750000000' }, + pending_open_local_balance: { sat: '500000', msat: '500000000' }, +})) +// The shell only renders the "Connect Your Wallet" QR + details when +// cert_base64url/macaroon_base64url and the ports are present. app.get('/lnd-connect-info', (_req, res) => res.json({ - getinfo: lndGetinfo(), channelCount: 4, cert_base: 'demo', + getinfo: lndGetinfo(), channelCount: 4, + cert_base64url: Buffer.from('demo-tls-cert-' + randomHex(24)).toString('base64url'), + macaroon_base64url: Buffer.from('demo-admin-macaroon-' + randomHex(32)).toString('base64url'), + grpc_port: 10009, rest_port: 8080, grpcReachable: true, restReachable: true, tor_onion: 'lnd' + randomHex(16) + '.onion', error: null, })) @@ -1458,6 +1611,13 @@ app.post('/rpc/v1', (req, res) => { return res.json({ result: { message: params?.message || 'Hello from Archipelago!' } }) } + case 'server.get-state': { + // Full state snapshot — the sync store polls this every 30s and after + // every WebSocket reconnect. Without it the UI can never resync (and + // logged a "Method not found" error each time). + return res.json({ result: { data: mockData } }) + } + case 'server.time': { return res.json({ result: { @@ -1485,7 +1645,7 @@ app.post('/rpc/v1', (req, res) => { title: 'Bitcoin Core', 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', + icon: '/assets/img/app-icons/bitcoin-core.png', author: 'Bitcoin Core', license: 'MIT', category: 'Bitcoin', @@ -1505,7 +1665,7 @@ app.post('/rpc/v1', (req, res) => { 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', + icon: '/assets/img/app-icons/electrumx.webp', author: 'Roman Zeyde', license: 'MIT', category: 'Bitcoin', @@ -1515,7 +1675,7 @@ app.post('/rpc/v1', (req, res) => { 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', + icon: '/assets/img/app-icons/mempool.webp', author: 'Mempool Space', license: 'AGPL-3.0', category: 'Bitcoin', @@ -1525,7 +1685,7 @@ app.post('/rpc/v1', (req, res) => { 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', + icon: '/assets/img/app-icons/btcpay-server.png', author: 'BTCPay Server', license: 'MIT', category: 'Commerce', @@ -1555,7 +1715,7 @@ app.post('/rpc/v1', (req, res) => { 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', + icon: '/assets/img/app-icons/vaultwarden.webp', author: 'Vaultwarden', license: 'AGPL-3.0', category: 'Privacy', @@ -1565,7 +1725,7 @@ app.post('/rpc/v1', (req, res) => { 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', + icon: '/assets/img/app-icons/nextcloud.webp', author: 'Nextcloud GmbH', license: 'AGPL-3.0', category: 'Cloud', @@ -1575,7 +1735,7 @@ app.post('/rpc/v1', (req, res) => { 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', + icon: '/assets/img/app-icons/nostrudel.svg', author: 'nostr-rs-relay', license: 'MIT', category: 'Social', @@ -1585,7 +1745,7 @@ app.post('/rpc/v1', (req, res) => { 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', + icon: '/assets/img/app-icons/homeassistant.png', author: 'Home Assistant', license: 'Apache-2.0', category: 'IoT', @@ -1595,7 +1755,7 @@ app.post('/rpc/v1', (req, res) => { 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', + icon: '/assets/img/app-icons/archipelago-a.svg', author: 'Syncthing Foundation', license: 'MPL-2.0', category: 'Cloud', @@ -1605,7 +1765,7 @@ app.post('/rpc/v1', (req, res) => { 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', + icon: '/assets/img/app-icons/tor.svg', author: 'Tor Project', license: 'BSD-3', category: 'Privacy', @@ -2015,6 +2175,20 @@ app.post('/rpc/v1', (req, res) => { return res.json({ result: { paid } }) } + // ── Networking profits: per-service pricing (interactive) ────────────── + case 'streaming.list-services': { + return res.json({ result: { services: mockState.streamingServices || [] } }) + } + case 'streaming.configure-service': { + const p = params || {} + const list = mockState.streamingServices || [] + const idx = list.findIndex(s => s.service_id === p.service_id) + if (idx >= 0) list[idx] = { ...list[idx], ...p } + else list.push({ accepted_mints: [], ...p }) + mockState.streamingServices = list + return res.json({ result: { success: true } }) + } + // ── Wallet settings: Cashu mints + Fedimint federations (interactive) ── case 'streaming.list-mints': { return res.json({ result: { mints: mockState.cashuMints } }) @@ -3363,51 +3537,24 @@ app.post('/rpc/v1', (req, res) => { // ── Monitoring ────────────────────────────────────────────── case 'monitoring.current': { - return res.json({ - result: { - system: { - cpu_percent: +(12 + Math.random() * 18).toFixed(1), - load_avg_1: +(0.5 + Math.random() * 1.5).toFixed(2), - load_avg_5: +(0.8 + Math.random()).toFixed(2), - load_avg_15: +(0.6 + Math.random()).toFixed(2), - mem_used_bytes: 6_200_000_000 + Math.floor(Math.random() * 500_000_000), - mem_total_bytes: 16_000_000_000, - disk_used_bytes: 620_000_000_000, - disk_total_bytes: 1_800_000_000_000, - net_rx_bytes: 12_400_000_000 + Math.floor(Math.random() * 100_000_000), - net_tx_bytes: 8_900_000_000 + Math.floor(Math.random() * 50_000_000), - uptime_secs: Math.floor(process.uptime()) + 604800, - }, - rpc: { avg_latency_ms: +(2 + Math.random() * 8).toFixed(1), requests_per_minute: Math.floor(30 + Math.random() * 40) }, - }, - }) + return res.json({ result: monitoringSnapshot(Math.floor(Date.now() / 1000)) }) } case 'monitoring.history': { - const points = 60 - const history = [] - for (let i = 0; i < points; i++) { - history.push({ - timestamp: new Date(Date.now() - (points - i) * 60000).toISOString(), - system: { - cpu_percent: +(10 + Math.random() * 25).toFixed(1), - mem_used_bytes: 5_800_000_000 + Math.floor(Math.random() * 1_000_000_000), - mem_total_bytes: 16_000_000_000, - net_rx_bytes: Math.floor(Math.random() * 5_000_000), - net_tx_bytes: Math.floor(Math.random() * 3_000_000), - }, - rpc: { avg_latency_ms: +(1 + Math.random() * 10).toFixed(1) }, - }) - } - return res.json({ result: { history } }) + const count = Math.min(params?.count || 60, 1440) + const nowSecs = Math.floor(Date.now() / 1000) + const data = Array.from({ length: count }, (_, i) => monitoringSnapshot(nowSecs - (count - 1 - i) * 60, i)) + return res.json({ result: { resolution: params?.resolution || 'minute', count: data.length, data } }) } case 'monitoring.alerts': { + const nowSecs = Math.floor(Date.now() / 1000) return res.json({ result: { alerts: [ - { id: 'a1', kind: 'cpu_high', message: 'CPU usage exceeded 80% for 5 minutes', timestamp: new Date(Date.now() - 7200000).toISOString(), acknowledged: false }, - { id: 'a2', kind: 'disk_high', message: 'Disk usage at 85%', timestamp: new Date(Date.now() - 86400000).toISOString(), acknowledged: true }, + { id: 'al-1', kind: 'ram_usage', message: 'Memory usage exceeded 85% (LND DB compaction)', value: 87.2, threshold: 85, timestamp: nowSecs - 7200, acknowledged: false }, + { id: 'al-2', kind: 'disk_usage', message: 'Disk usage reached 80% on /var/lib/archipelago', value: 80.4, threshold: 80, timestamp: nowSecs - 86400, acknowledged: true }, + { id: 'al-3', kind: 'backend_error_spike', message: 'RPC latency spiked to 620ms', value: 620, threshold: 500, timestamp: nowSecs - 172800, acknowledged: true }, ], }, }) @@ -3417,10 +3564,11 @@ app.post('/rpc/v1', (req, res) => { return res.json({ result: { rules: [ - { kind: 'cpu_high', enabled: true, threshold: 80, description: 'Alert when CPU usage exceeds threshold' }, - { kind: 'mem_high', enabled: true, threshold: 85, description: 'Alert when memory usage exceeds threshold' }, - { kind: 'disk_high', enabled: true, threshold: 90, description: 'Alert when disk usage exceeds threshold' }, - { kind: 'backend_error_spike', enabled: false, threshold: 500, description: 'Alert when RPC latency exceeds threshold' }, + { kind: 'disk_usage', enabled: true, threshold: 80, description: 'Disk usage above threshold (%)' }, + { kind: 'ram_usage', enabled: true, threshold: 85, description: 'Memory usage above threshold (%)' }, + { kind: 'container_crash', enabled: true, threshold: 1, description: 'Container restarted unexpectedly' }, + { kind: 'backend_error_spike', enabled: false, threshold: 500, description: 'RPC latency above threshold (ms)' }, + { kind: 'ssl_cert_expiry', enabled: true, threshold: 14, description: 'TLS certificate expires within N days' }, ], }, }) @@ -3432,7 +3580,16 @@ app.post('/rpc/v1', (req, res) => { } case 'monitoring.export': { - return res.json({ result: { data: 'timestamp,cpu,mem\n2026-04-10T12:00:00Z,15.2,38.5\n' } }) + const count = Math.min(params?.count || 1440, 1440) + const nowSecs = Math.floor(Date.now() / 1000) + const data = Array.from({ length: count }, (_, i) => monitoringSnapshot(nowSecs - (count - 1 - i) * 60, i)) + if (params?.format === 'csv') { + const csv = ['timestamp,cpu_percent,mem_used_bytes,mem_total_bytes,net_rx_bytes,net_tx_bytes,rpc_latency_ms'] + .concat(data.map(s => [s.timestamp, s.system.cpu_percent, s.system.mem_used_bytes, s.system.mem_total_bytes, s.system.net_rx_bytes, s.system.net_tx_bytes, s.rpc_latency_ms].join(','))) + .join('\n') + return res.json({ result: { csv, count: data.length } }) + } + return res.json({ result: { data, count: data.length } }) } case 'monitoring.container-metrics': { @@ -3537,7 +3694,6 @@ const SEED_FILES = { { name: 'Music', path: '/Music', size: 0, modified: '2025-03-01T10:00:00Z', isDir: true, type: '' }, { name: 'Documents', path: '/Documents', size: 0, modified: '2025-02-28T14:30:00Z', isDir: true, type: '' }, { name: 'Photos', path: '/Photos', size: 0, modified: '2025-02-20T09:15:00Z', isDir: true, type: '' }, - { name: 'Videos', path: '/Videos', size: 0, modified: '2025-01-15T18:00:00Z', isDir: true, type: '' }, ], '/Music': [ { name: 'Bad Actors Reveal.mp3', path: '/Music/Bad Actors Reveal.mp3', size: 8_400_000, modified: '2025-01-10T12:00:00Z', isDir: false, type: 'audio' }, @@ -3572,11 +3728,8 @@ const SEED_FILES = { { name: 'home-server-build.jpg', path: '/Photos/home-server-build.jpg', size: 2_900_000, modified: '2024-11-20T16:45:00Z', isDir: false, type: 'image' }, { name: 'sunset-from-balcony.jpg', path: '/Photos/sunset-from-balcony.jpg', size: 4_200_000, modified: '2025-02-14T18:30:00Z', isDir: false, type: 'image' }, ], - '/Videos': [ - { name: 'node-unboxing-timelapse.mp4', path: '/Videos/node-unboxing-timelapse.mp4', size: 85_000_000, modified: '2024-11-01T10:00:00Z', isDir: false, type: 'video' }, - { name: 'bitcoin-explained-5min.mp4', path: '/Videos/bitcoin-explained-5min.mp4', size: 42_000_000, modified: '2024-10-15T14:00:00Z', isDir: false, type: 'video' }, - { name: 'lightning-payment-demo.mp4', path: '/Videos/lightning-payment-demo.mp4', size: 28_000_000, modified: '2025-01-20T12:00:00Z', isDir: false, type: 'video' }, - ], + // NOTE: no seeded Videos folder — every seeded file must be backed by real, + // playable content (curated videos can still be added via demo/files/Videos). } const SEED_FILE_CONTENTS = { @@ -3587,19 +3740,35 @@ const SEED_FILE_CONTENTS = { '/Documents/backup-log.json': JSON.stringify({ backups: [{ id: 'bkp-2025-03-01', timestamp: '2025-03-01T02:00:00Z', type: 'full', apps: ['bitcoin-knots', 'lnd', 'mempool'], size_mb: 2340, status: 'success' }] }, null, 2), } -// Curated real files: drop files into /demo/files// and they -// replace the seeded cloud content for every visitor (read-only — visitors can -// view/download/buy them but only maintainers add them, via git = the "private -// login"). If the folder is absent/empty the hardcoded seeds above are kept. -// Binary files are streamed from disk on demand (diskFilePaths); text is inlined. +// Curated real files: drop files into /demo/files// (or the +// committed library in /demo/content/) and they replace the seeded cloud +// content for every visitor (read-only — visitors can view/download/buy them but +// only maintainers add them, via git = the "private login"). If both are +// absent/empty the hardcoded seeds above are kept. Binary files are streamed +// from disk on demand (diskFilePaths); text is inlined. const diskFilePaths = {} function loadDemoDiskFiles() { - const root = path.join(__dirname, '..', 'demo', 'files') - let top - try { top = fsSync.readdirSync(root, { withFileTypes: true }) } catch { return } + // demo/files (drop-in overrides) wins over demo/content (committed library) + // when both provide the same top-level folder. + const roots = [ + path.join(__dirname, '..', 'demo', 'files'), + path.join(__dirname, '..', 'demo', 'content'), + ] const tree = { '/': [] } const contents = {} const TEXT_EXT = new Set(['txt', 'md', 'json', 'csv', 'log', 'yaml', 'yml', 'xml', 'conf', 'ini']) + const addFile = (abs, relDir, name) => { + let st; try { st = fsSync.statSync(abs) } catch { return } + const ext = (name.includes('.') ? name.split('.').pop() : '').toLowerCase() + const type = fbType(name) + const rel = relDir === '/' ? `/${name}` : `${relDir}/${name}` + tree[relDir].push({ name, path: rel, size: st.size, modified: st.mtime.toISOString(), isDir: false, type }) + if (TEXT_EXT.has(ext) && st.size < 1_000_000) { + try { contents[rel] = fsSync.readFileSync(abs, 'utf-8') } catch { /* skip */ } + } else { + diskFilePaths[rel] = abs // streamed from disk by the raw handler + } + } const walk = (absDir, relDir) => { let entries try { entries = fsSync.readdirSync(absDir, { withFileTypes: true }) } catch { return } @@ -3607,25 +3776,36 @@ function loadDemoDiskFiles() { for (const e of entries) { if (e.name.startsWith('.')) continue const abs = path.join(absDir, e.name) - const rel = relDir === '/' ? `/${e.name}` : `${relDir}/${e.name}` if (e.isDirectory()) { + const rel = relDir === '/' ? `/${e.name}` : `${relDir}/${e.name}` tree[relDir].push({ name: e.name, path: rel, size: 0, modified: new Date().toISOString(), isDir: true, type: '' }) walk(abs, rel) } else { - let st; try { st = fsSync.statSync(abs) } catch { continue } - const ext = (e.name.includes('.') ? e.name.split('.').pop() : '').toLowerCase() - const type = fbType(e.name) - tree[relDir].push({ name: e.name, path: rel, size: st.size, modified: st.mtime.toISOString(), isDir: false, type }) - if (TEXT_EXT.has(ext) && st.size < 1_000_000) { - try { contents[rel] = fsSync.readFileSync(abs, 'utf-8') } catch { /* skip */ } - } else { - diskFilePaths[rel] = abs // streamed from disk by the raw handler - } + addFile(abs, relDir, e.name) } } } - walk(root, '/') - if (!tree['/'].length) return // empty folder → keep the hardcoded seeds + const claimed = new Set() + for (const root of roots) { + let top + try { top = fsSync.readdirSync(root, { withFileTypes: true }) } catch { continue } + for (const e of top) { + if (e.name.startsWith('.')) continue + if (e.isDirectory()) { + // Normalize top-level folder names for display/merge (music → Music). + const display = e.name.charAt(0).toUpperCase() + e.name.slice(1) + if (claimed.has(display)) continue + claimed.add(display) + const rel = `/${display}` + tree['/'].push({ name: display, path: rel, size: 0, modified: new Date().toISOString(), isDir: true, type: '' }) + tree[rel] = tree[rel] || [] + walk(path.join(root, e.name), rel) + } else if (!tree['/'].some(x => x.name === e.name)) { + addFile(path.join(root, e.name), '/', e.name) + } + } + } + if (!tree['/'].length) return // nothing curated → keep the hardcoded seeds // Per-folder MERGE: a top-level folder present in demo/files replaces that // seed folder; seed folders the curator didn't provide (e.g. sample Documents) // are kept. So dropping real Music/ doesn't wipe the sample Documents. @@ -3828,11 +4008,72 @@ app.get('/app/filebrowser/api/raw/*', (req, res) => { .on('error', () => { if (!res.headersSent) res.status(404).send('File not found') }).pipe(res) } const content = currentStore().files.contents[full] - if (content === undefined) return res.status(404).send('File not found') + if (content === undefined) { + // Seeded media entries have no backing file (only curated demo/content or + // demo/files do). Serve generated-but-real media so nothing 404s and the + // players never hit "no supported source". + const type = fbType(fbBase(full)) + if (type === 'audio') { + const wav = demoToneWav() + res.type('audio/wav') + res.setHeader('Content-Length', wav.length) + return res.send(wav) + } + if (type === 'image') { + res.type('image/svg+xml') + return res.send(demoPlaceholderSvg(fbBase(full))) + } + return res.status(404).send('File not found') + } res.type(fbContentType(fbBase(full))) res.send(Buffer.isBuffer(content) ? content : String(content)) }) +// A short, real WAV (two-note ambient pad) used for seeded audio entries that +// have no curated file behind them — so pressing play always plays something. +let __demoToneWav = null +function demoToneWav() { + if (__demoToneWav) return __demoToneWav + const rate = 22050 + const secs = 20 + const n = rate * secs + const buf = Buffer.alloc(44 + n * 2) + buf.write('RIFF', 0) + buf.writeUInt32LE(36 + n * 2, 4) + buf.write('WAVEfmt ', 8) + buf.writeUInt32LE(16, 16) // fmt chunk size + buf.writeUInt16LE(1, 20) // PCM + buf.writeUInt16LE(1, 22) // mono + buf.writeUInt32LE(rate, 24) + buf.writeUInt32LE(rate * 2, 28) // byte rate + buf.writeUInt16LE(2, 32) // block align + buf.writeUInt16LE(16, 34) // bits per sample + buf.write('data', 36) + buf.writeUInt32LE(n * 2, 40) + for (let i = 0; i < n; i++) { + const t = i / rate + const swell = 0.6 + 0.4 * Math.sin(2 * Math.PI * t / 8) + const v = (Math.sin(2 * Math.PI * 220 * t) * 0.28 + Math.sin(2 * Math.PI * 277.18 * t) * 0.2) * swell + const fade = Math.min(1, t / 0.1, (secs - t) / 0.5) + buf.writeInt16LE(Math.round(v * fade * 16000), 44 + i * 2) + } + __demoToneWav = buf + return buf +} + +// Neutral placeholder for seeded image entries without a curated file. +function demoPlaceholderSvg(name) { + const label = String(name).replace(/[<>&"]/g, '') + return ` + + + + + ${label} + Archipelago demo sample +` +} + // A compact description of the current (mock) node, fed to the AI assistant as // system context in the demo so it can answer questions about this node. function demoNodeContext() { @@ -4231,6 +4472,126 @@ app.get('/app/thunderhub/api/invoices', (req, res) => res.json(MOCK_LND_DATA.inv app.get('/app/thunderhub/api/payments', (req, res) => res.json(MOCK_LND_DATA.payments)) app.get('/app/thunderhub/api/forwards', (req, res) => res.json(MOCK_LND_DATA.forwarding)) +// ── Demo placeholder UIs for the extra installed apps ──────────────────────── +// Static dashboards rendered with demoAppShell so every installed app opens to +// something plausible in the in-app iframe. Registered before the generic +// /app/:id notice handler so these win. +const DEMO_APP_PAGES = { + 'btcpay-server': () => demoAppShell('BTCPay Server', 'Self-hosted payment processor · signet', '/assets/img/app-icons/btcpay-server.png', ` +
+
Store
Archipelago Shop
+
Invoices (30d)
47
+
Settled
1,284,500 sats
+
Lightning
Connected
+
+
+ + + + + +
InvoiceAmountStatusCreated
INV-0047 · Sticker pack21,000 satsSettled2h ago
INV-0046 · Node consult250,000 satsSettled9h ago
INV-0045 · Coffee5,500 satsSettled1d ago
INV-0044 · Merch bundle84,000 satsExpired2d ago
`), + grafana: () => demoAppShell('Grafana', 'Dashboards · node metrics', '/assets/img/app-icons/grafana.png', ` +
+
Dashboards
6
+
Data sources
3
+
Alerts firing
0
+
Uptime
99.98%
+
+
+ + + + + +
DashboardPanelsLast viewed
Bitcoin Node Overview1412m ago
Lightning Routing91h ago
System Resources113h ago
Tor & Network71d ago
`), + nextcloud: () => demoAppShell('Nextcloud', 'Files · Calendar · Contacts', '/assets/img/app-icons/nextcloud.webp', ` +
+
Storage used
182 GB / 1.8 TB
+
Files
12,847
+
Shares
23
+
Users
3
+
+
+ + + + +
Recent fileSizeModified
node-backup-2026-07.tar.gz2.3 GBToday
Sovereign Computing.pdf4.8 MBYesterday
family-photos-june/1.2 GB3d ago
`), + jellyfin: () => demoAppShell('Jellyfin', 'The Free Software Media System', '/assets/img/app-icons/jellyfin.webp', ` +
+
Movies
214
+
Shows
38
+
Music albums
156
+
Active streams
1
+
+
+ + + + +
Recently addedTypeAdded
The Bitcoin Standard (audiobook)AudioToday
Lightning Payment DemoVideo2d ago
Node Unboxing TimelapseVideo5d ago
`), + vaultwarden: () => demoAppShell('Vaultwarden', 'Bitwarden-compatible password manager', '/assets/img/app-icons/vaultwarden.webp', ` +
+
Vault items
184
+
Logins
142
+
Secure notes
28
+
Status
Vault locked
+
+
+
Security report
+ + + + + +
Weak passwords3
Reused passwords1
Exposed in breaches0
Two-factor enabledYes
+
`), + 'nostr-rs-relay': () => demoAppShell('Nostr Relay', 'wss://relay.archipelago.lan', '/assets/img/app-icons/nostrudel.svg', ` +
+
Events stored
48,213
+
Connections
17
+
Subscriptions
42
+
DB size
312 MB
+
+
+ + + + + +
KindDescriptionEvents (24h)
1Short text notes1,824
0Profile metadata96
7Reactions3,412
4Encrypted DMs58
`), + searxng: () => demoAppShell('SearXNG', 'Privacy-respecting metasearch', '/assets/img/app-icons/searxng.png', ` +
+ +
No queries logged · no profiling · results proxied
+
+
+
Engines enabled
28
+
Queries today
64
+
Avg response
0.8s
+
Tracking
Zero
+
`), + 'uptime-kuma': () => demoAppShell('Uptime Kuma', 'Service monitoring', '/assets/img/app-icons/uptime-kuma.webp', ` +
+
Monitors
8
+
Up
8
+
Avg uptime (30d)
99.94%
+
Incidents
1
+
+
+ + + + + + +
MonitorStatusUptimePing
Bitcoin RPCUp100%4 ms
LND RESTUp99.9%7 ms
Electrum serverUp99.8%11 ms
Tor hidden serviceUp99.7%380 ms
Mempool webUp100%6 ms
`), +} +for (const [id, page] of Object.entries(DEMO_APP_PAGES)) { + app.get([`/app/${id}`, `/app/${id}/`], (_req, res) => res.type('html').send(page())) +} + // Generic app shell for any launched app without a dedicated mock UI — a clean // "not interactive in the demo" notice with the app's icon. Registered after all // specific /app/... routes so those win; only bare /app/[/] reaches here. @@ -4406,16 +4767,18 @@ wss.on('connection', (ws, req) => { clearInterval(pingInterval) clearInterval(heartbeatInterval) } - }, 30000) // Ping every 30 seconds + }, 25000) // Ping every 25 seconds - // Send periodic heartbeat data so clients don't think the connection is dead + // Send periodic heartbeat data so clients (and any idle-timeout proxy in + // front of the public demo) never see a silent socket. 20s stays well under + // common 30–60s proxy idle timeouts that were killing demo connections. const heartbeatInterval = setInterval(() => { if (ws.readyState === 1) { try { ws.send(JSON.stringify({ type: 'heartbeat', rev: Date.now() })) } catch { /* ignore */ } } - }, 45000) // Every 45s (client expects data within 60s) + }, 20000) // Send initial data immediately (this visitor's store, not the global proxy — // there is no request context inside the WS connection handler). @@ -4458,15 +4821,16 @@ wss.on('connection', (ws, req) => { }) }) -// Best-effort: pull a few REAL recent testnet txids so the wallet's transactions -// deep-link to live mempool.space/testnet pages. Falls back to the mock hashes -// (already set) if offline. Patches the pristine seed so every session inherits them. +// Best-effort: pull a few REAL recent txids from mempool.guide (the instance the +// demo's in-app explorer proxies) so wallet transactions deep-link to pages that +// actually resolve. Falls back to the mock hashes (already set) if offline. +// Patches the pristine seed so every session inherits them. async function hydrateRealTestnetTxids() { if (!DEMO) return try { const ctrl = new AbortController() const t = setTimeout(() => ctrl.abort(), 4000) - const r = await fetch('https://mempool.space/testnet/api/mempool/recent', { signal: ctrl.signal }) + const r = await fetch('https://mempool.guide/api/mempool/recent', { signal: ctrl.signal }) clearTimeout(t) if (!r.ok) return const recent = await r.json() diff --git a/neode-ui/src/components/LightningChannelsPanel.vue b/neode-ui/src/components/LightningChannelsPanel.vue index 7adb8057..fe1caf0b 100644 --- a/neode-ui/src/components/LightningChannelsPanel.vue +++ b/neode-ui/src/components/LightningChannelsPanel.vue @@ -258,13 +258,11 @@ diff --git a/neode-ui/vite.config.ts b/neode-ui/vite.config.ts index 67149bd7..bb1b14eb 100644 --- a/neode-ui/vite.config.ts +++ b/neode-ui/vite.config.ts @@ -141,8 +141,10 @@ export default defineConfig({ changeOrigin: true, secure: false, }, + // Mock filebrowser (Cloud page) — same backend as everything else. Point + // BACKEND_URL at a real node to develop against its filebrowser instead. '/app/filebrowser': { - target: 'http://192.168.1.228', + target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false, }, @@ -158,6 +160,10 @@ export default defineConfig({ '/app/fedimint': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false }, '/app/bitcoin-core': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false }, '/app/bitcoin-knots': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false }, + // Catch-all for the remaining mock app UIs (btcpay-server, grafana, …) and + // the generic "Not available in the demo" notice. Registered after the + // specific /app/* keys so those still win. + '/app': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false }, '/electrs-status': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false }, '/proxy': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false }, '/lnd-connect-info': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false },