2026-01-24 22:59:20 +00:00
#!/usr/bin/env node
/ * *
* Archipelago Mock Backend Server
* Pure Archipelago implementation - NO StartOS dependencies
* Supports dev modes : setup , onboarding , existing
* /
import express from 'express'
import cors from 'cors'
import cookieParser from 'cookie-parser'
import { WebSocketServer } from 'ws'
import http from 'http'
import { exec } from 'child_process'
import { promisify } from 'util'
import fs from 'fs/promises'
import path from 'path'
import { fileURLToPath } from 'url'
2026-01-27 23:06:18 +00:00
import Docker from 'dockerode'
2026-06-22 09:28:05 -04:00
import { AsyncLocalStorage } from 'node:async_hooks'
import crypto from 'crypto'
2026-01-24 22:59:20 +00:00
const _ _filename = fileURLToPath ( import . meta . url )
const _ _dirname = path . dirname ( _ _filename )
const execPromise = promisify ( exec )
2026-03-18 21:06:14 +00:00
2026-06-22 09:28:05 -04:00
// DEMO mode: public, multi-visitor sandbox. Each visitor gets an isolated,
// ephemeral copy of all mutable state (see per-session store below), real
// container runtimes are never touched, and idle sessions are reaped.
// When DEMO is off, behaviour is identical to the classic single-user dev mock.
const DEMO =
process . env . DEMO === '1' ||
process . env . VITE _DEMO === '1' ||
process . env . VITE _DEV _MODE === 'demo'
2026-03-18 21:06:14 +00:00
// Find container socket: Podman (macOS/Linux) or Docker
2026-06-22 09:28:05 -04:00
import { existsSync , readFileSync } from 'fs'
2026-06-22 11:11:40 -04:00
import * as fsSync from 'fs'
2026-06-22 09:28:05 -04:00
// Report the real app version, suffixed with -demo in the public sandbox so it's
// obviously the demo while still tracking whatever version the UI ships.
let APP _VERSION = '0.1.0'
try {
const pkg = JSON . parse ( readFileSync ( new URL ( './package.json' , import . meta . url ) , 'utf-8' ) )
if ( pkg . version ) APP _VERSION = pkg . version
} catch { /* fall back to default */ }
if ( DEMO ) APP _VERSION += '-demo'
2026-03-18 21:06:14 +00:00
function findContainerSocket ( ) {
// DOCKER_HOST env var (set by podman machine start)
if ( process . env . DOCKER _HOST ) {
const p = process . env . DOCKER _HOST . replace ( 'unix://' , '' )
if ( existsSync ( p ) ) return p
}
// Podman machine socket (macOS) — check TMPDIR-based path
if ( process . env . TMPDIR ) {
const podmanDir = path . join ( path . dirname ( process . env . TMPDIR ) , 'podman' )
const sock = path . join ( podmanDir , 'podman-machine-default-api.sock' )
if ( existsSync ( sock ) ) return sock
}
// Docker socket
if ( existsSync ( '/var/run/docker.sock' ) ) return '/var/run/docker.sock'
// Linux podman rootless
const uid = process . getuid ? . ( ) || 1000
const linuxSock = ` /run/user/ ${ uid } /podman/podman.sock `
if ( existsSync ( linuxSock ) ) return linuxSock
return null
}
2026-06-22 09:28:05 -04:00
// In DEMO mode we never bind to a real container runtime — the public demo must
// be host-independent and unable to touch the host's Docker/Podman.
const containerSocket = DEMO ? null : findContainerSocket ( )
2026-03-18 21:06:14 +00:00
const docker = containerSocket ? new Docker ( { socketPath : containerSocket } ) : null
2026-06-22 09:28:05 -04:00
if ( DEMO ) {
console . log ( '[Container] DEMO mode — simulation only (real runtime disabled)' )
} else if ( containerSocket ) {
2026-03-18 21:06:14 +00:00
console . log ( ` [Container] Socket: ${ containerSocket } ` )
} else {
console . log ( '[Container] No socket found — simulation mode (no Docker/Podman)' )
}
2026-01-24 22:59:20 +00:00
const app = express ( )
const PORT = 5959
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:56:37 +00:00
// Dev mode from environment (setup, onboarding, existing, boot, or default)
2026-01-24 22:59:20 +00:00
const DEV _MODE = process . env . VITE _DEV _MODE || 'default'
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:56:37 +00:00
// Boot mode: simulate server startup delay
let BOOT _START _TIME = Date . now ( )
const BOOT _DELAY _MS = 25000 // 25 seconds of simulated startup (slower for analysis)
2026-01-24 22:59:20 +00:00
// CORS configuration
const corsOptions = {
credentials : true ,
origin : ( origin , callback ) => {
if ( ! origin ) return callback ( null , true )
callback ( null , true )
}
}
app . use ( cors ( corsOptions ) )
2026-03-09 21:27:26 +00:00
// Skip JSON body parsing for filebrowser upload routes (binary file bodies)
app . use ( ( req , res , next ) => {
if ( req . path . startsWith ( '/app/filebrowser/api/resources' ) && req . method === 'POST' ) {
return next ( )
}
express . json ( { limit : '50mb' } ) ( req , res , next )
} )
2026-01-24 22:59:20 +00:00
app . use ( cookieParser ( ) )
2026-06-22 09:28:05 -04:00
// DEMO: bind every request to an isolated per-visitor state store (keyed by the
// `demo_sid` cookie) for the remainder of the request. Outside DEMO this is a
// no-op and all handlers share the single default store (classic mock behaviour).
app . use ( ( req , res , next ) => {
if ( ! DEMO ) return next ( )
const store = resolveSessionStore ( req , res )
stateContext . run ( store , ( ) => next ( ) )
} )
2026-01-24 22:59:20 +00:00
// Mock session storage
const sessions = new Map ( )
2026-06-22 09:28:05 -04:00
// Public demo uses a memorable shared password (shown on the login screen);
// the classic dev mock keeps password123.
const MOCK _PASSWORD = DEMO ? 'entertoexit' : 'password123'
// Mutable wallet state — faucet/send/receive modify these values.
// SEED_* objects are pristine templates; each demo session gets a deep clone
// (see makeSessionStore). `walletState` itself becomes a session-aware proxy below.
const SEED _WALLET = {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
onchain _sats : 2_350_000 ,
channel _sats : 8_250_000 ,
ecash _sats : 250_000 ,
ecash _tokens : 12 ,
feat(ui): Ark wallet tab, balances, history badge + demo mocks
Ark tab in Wallet Settings (status, balances, receive address,
board/offboard, server config via wallet.ark-*), teal Ark badge in the
transactions modal, Ark row on the home wallet card (hidden until barkd
reports a balance), official bark icon, and wallet.ark-* mocks so the
public demo renders the tab.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 21:57:12 +01:00
// Ark (barkd sidecar) demo balances — signet defaults
ark _sats : 75_000 ,
ark _pending _sats : 0 ,
ark _onchain _sats : 20_000 ,
ark _config : { network : 'signet' , ark _server : 'https://ark.signet.2nd.dev' , esplora : 'https://esplora.signet.2nd.dev' } ,
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
block _height : 892451 ,
transactions : [
{ tx _hash : 'ab12cd34ef5678901234567890abcdef12345678' , amount _sats : 2_000_000 , direction : 'incoming' , num _confirmations : 142 , block _height : 892310 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) - 86400 , label : 'Channel funding' , total _fees : 0 , dest _addresses : [ ] } ,
{ tx _hash : 'cd34ef5678901234567890abcdef1234567890ab' , amount _sats : 250_000 , direction : 'incoming' , num _confirmations : 28 , block _height : 892420 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) - 7200 , label : 'Faucet deposit' , total _fees : 0 , dest _addresses : [ ] } ,
{ tx _hash : 'ff99ee88dd7766554433221100aabbccddeeff00' , amount _sats : 100_000 , direction : 'incoming' , num _confirmations : 0 , block _height : 0 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) - 600 , label : 'Incoming from faucet' , total _fees : 0 , dest _addresses : [ ] } ,
feat(demo): network/wallet dummy data — profits, federation, VPN, nostr, visibility
- wallet.networking-profits = 5,231,978 sats (content 3,180,000 / routing
1,281,978 / relay 770,000); 6 labelled profit transactions added to the wallet
history (1-2 per type: content sale, routing fee, file/mesh relay) — labels are
production-ready.
- federation.list (the Web5 Federation container's method) now returns the 12
demo nodes (was unhandled → empty).
- vpn.status: connected WireGuard with peers + traffic.
- nostr.list-relays / nostr.get-stats: 5 relays (3 connected).
- network.get/set-visibility: interactive, persisted per demo session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:18:29 -04:00
// Networking profits — one or two per type, with human labels (these labels
// are intended for the production wallet UI too).
{ tx _hash : '1a2b3c4d5e6f78901234567890abcdef11111111' , amount _sats : 480_000 , direction : 'incoming' , num _confirmations : 64 , block _height : 892387 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) - 18000 , label : 'Content sale · “Lightning Demo.mp4”' , profit _type : 'content_sales' , total _fees : 0 , dest _addresses : [ ] } ,
{ tx _hash : '2b3c4d5e6f7890abcdef1234567890abcd222222' , amount _sats : 312_500 , direction : 'incoming' , num _confirmations : 110 , block _height : 892341 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) - 43200 , label : 'Content sale · “Sovereign Computing.pdf”' , profit _type : 'content_sales' , total _fees : 0 , dest _addresses : [ ] } ,
{ tx _hash : '3c4d5e6f7890abcdef1234567890abcdef333333' , amount _sats : 1_842 , direction : 'incoming' , num _confirmations : 12 , block _height : 892439 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) - 3600 , label : 'Lightning routing fee' , profit _type : 'routing_fees' , total _fees : 0 , dest _addresses : [ ] } ,
{ tx _hash : '4d5e6f7890abcdef1234567890abcdef44444444' , amount _sats : 906 , direction : 'incoming' , num _confirmations : 5 , block _height : 892446 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) - 1500 , label : 'Lightning routing fee' , profit _type : 'routing_fees' , total _fees : 0 , dest _addresses : [ ] } ,
{ tx _hash : '5e6f7890abcdef1234567890abcdef5555555555' , amount _sats : 64_200 , direction : 'incoming' , num _confirmations : 38 , block _height : 892410 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) - 9000 , label : 'File relay reward' , profit _type : 'relay' , total _fees : 0 , dest _addresses : [ ] } ,
{ tx _hash : '6f7890abcdef1234567890abcdef666666666666' , amount _sats : 28_750 , direction : 'incoming' , num _confirmations : 21 , block _height : 892428 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) - 5400 , label : 'Mesh relay forwarding fee' , profit _type : 'relay' , total _fees : 0 , dest _addresses : [ ] } ,
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
] ,
}
function randomHex ( bytes ) { return Array . from ( { length : bytes } , ( ) => Math . floor ( Math . random ( ) * 256 ) . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) }
2026-06-22 16:34:12 -04:00
// Tracks when a content invoice was requested, so the demo can leave the QR on
// screen for a couple of seconds before reporting it paid.
const invoiceRequestedAt = new Map ( )
2026-06-22 09:28:05 -04:00
const SEED _BTCRELAY = {
2026-06-11 00:24:40 -04:00
settings : {
enabled _for _peers : true ,
allow _peer _requests : true ,
allow _http : false ,
allow _https : true ,
allow _tor : true ,
selected _peer _pubkey : '03d9b8a8db6b4f4d8b8d04c7a467c101f04c0ecbabc0e29e4dcb812a3b1c5f8f04' ,
http _endpoint : '' ,
2026-06-22 13:55:50 -04:00
https _endpoint : 'https://relay-' + randomHex ( 5 ) + '.example.net/' ,
2026-06-11 00:24:40 -04:00
tor _endpoint : 'http://btc-relay-demoabcdefghijklmnop.onion/' ,
} ,
trusted _nodes : [
{
pubkey : '03d9b8a8db6b4f4d8b8d04c7a467c101f04c0ecbabc0e29e4dcb812a3b1c5f8f04' ,
onion : 'trustedalphaabcdefghijklmnop.onion' ,
name : 'Trusted Alpha' ,
relay _approved : true ,
} ,
{
pubkey : '02f6ab6c88037cd527a92f3a016a7bd18bb2ebd91d5a3efb1161481b5cf7d9ea2a' ,
onion : 'trustedbetabcdefghijklmnopq.onion' ,
name : 'Trusted Beta' ,
relay _approved : false ,
} ,
] ,
requests : [
{
id : 'relay-demo-incoming' ,
direction : 'incoming' ,
status : 'pending' ,
peer _pubkey : '02f6ab6c88037cd527a92f3a016a7bd18bb2ebd91d5a3efb1161481b5cf7d9ea2a' ,
peer _onion : 'trustedbetabcdefghijklmnopq.onion' ,
peer _name : 'Trusted Beta' ,
message : 'Can I use your node for a Wasabi broadcast?' ,
created _at : new Date ( Date . now ( ) - 12 * 60 * 1000 ) . toISOString ( ) ,
updated _at : new Date ( Date . now ( ) - 12 * 60 * 1000 ) . toISOString ( ) ,
} ,
] ,
}
2026-06-22 09:28:05 -04:00
// User state (simulated file-based storage). Returns a fresh object per session.
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
// In DEMO the visitor is always treated as fully set up ("existing") so the
// onboarding WIZARD (seed/identity/backup) is never forced by the route guard.
// The welcome INTRO still shows via the frontend's per-day replay gate.
2026-06-22 09:28:05 -04:00
function seedUserState ( ) {
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
const mode = DEMO ? 'existing' : DEV _MODE
2026-06-22 09:28:05 -04:00
switch ( mode ) {
2026-01-24 22:59:20 +00:00
case 'setup' :
2026-06-22 09:28:05 -04:00
// Setup mode: user needs to set a password (simple setup, not onboarding).
return { setupComplete : false , onboardingComplete : false , passwordHash : null }
2026-01-24 22:59:20 +00:00
case 'onboarding' :
2026-06-22 09:28:05 -04:00
// Password already set; visitor still needs to go through onboarding/intro.
return { setupComplete : true , onboardingComplete : false , passwordHash : MOCK _PASSWORD }
2026-01-24 22:59:20 +00:00
case 'existing' :
2026-06-22 09:28:05 -04:00
// Fully set up, just needs to log in.
return { setupComplete : true , onboardingComplete : true , passwordHash : MOCK _PASSWORD }
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:56:37 +00:00
case 'boot' :
2026-06-22 09:28:05 -04:00
// Simulate server startup delay (boot screen), then behave like onboarding.
return { setupComplete : true , onboardingComplete : false , passwordHash : MOCK _PASSWORD }
2026-01-24 22:59:20 +00:00
default :
2026-06-22 09:28:05 -04:00
// Default: fully set up (for UI development).
return { setupComplete : true , onboardingComplete : true , passwordHash : MOCK _PASSWORD }
2026-01-24 22:59:20 +00:00
}
}
2026-06-22 09:28:05 -04:00
function seedMockState ( ) {
2026-06-22 16:34:12 -04:00
return {
analyticsEnabled : false ,
nodeVisibility : 'discoverable' ,
2026-07-22 02:55:06 -04:00
// Discovery starts OFF, matching the production default — enabling it in
// the UI walks through the presence-signing overlay.
nostrDiscovery : false ,
2026-07-14 06:40:26 -04:00
pendingPeerRequests : [
{
id : 'preq-demo-1' ,
from _nostr _pubkey : '7e2a9c4b1d8f3a6c5e0b9d2f7a4c1e8b3d6f9a2c5e8b1d4f7a0c3e6b9d2f5a8c' ,
from _nostr _npub : 'npub1demo0inbound0request0xyzq' ,
from _did : 'did:key:z6MkwPn5xQc8vT2sLb6eRu9dYf3gHa7mJzK1oDiW4nXvE8tq' ,
from _name : 'basement-node' ,
message : 'Hey — saw your node on the relay, mind if we peer?' ,
received _at : new Date ( Date . now ( ) - 45 * 60000 ) . toISOString ( ) ,
state : 'pending' ,
outbound : false ,
} ,
] ,
tollgateEnabled : true ,
tollgatePrice : 21 ,
tollgateStepMs : 60000 ,
tollgateMinSteps : 5 ,
tollgateMint : 'https://mint.archy.demo' ,
2026-06-22 16:34:12 -04:00
cashuMints : [
'https://mint.minibits.cash/Bitcoin' ,
'https://stablenut.umint.cash' ,
'https://mint.coinos.io' ,
'https://8333.space:3338' ,
] ,
federations : [
{ 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 } ,
] ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
// 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 : [ ] } ,
] ,
2026-06-22 16:34:12 -04:00
}
2026-06-22 09:28:05 -04:00
}
2026-01-24 22:59:20 +00:00
2026-06-22 09:28:05 -04:00
console . log ( ` [Auth] Dev mode: ${ DEV _MODE } ${ DEMO ? ' (DEMO multi-session)' : '' } ` )
2026-01-24 22:59:20 +00:00
2026-06-22 09:28:05 -04:00
// Broadcast a data-update patch to the WebSocket clients of the CURRENT session
// only (so demo visitors never see each other's state). Outside a request context
// (e.g. startup) this resolves to the default store, matching single-user mode.
2026-01-24 22:59:20 +00:00
function broadcastUpdate ( patch ) {
const message = JSON . stringify ( {
rev : Date . now ( ) ,
patch : patch
} )
2026-06-22 09:28:05 -04:00
currentStore ( ) . sockets . forEach ( client => {
2026-01-24 22:59:20 +00:00
if ( client . readyState === 1 ) { // OPEN
client . send ( message )
}
} )
}
// Track used ports and running containers
const usedPorts = new Set ( [ 5959 , 8100 ] )
const runningContainers = new Map ( )
// Predefined port mappings for known apps
const portMappings = {
'atob' : 8102 ,
'k484' : 8103 ,
2026-03-09 17:09:59 +00:00
'amin' : 8104 ,
'filebrowser' : 8083 ,
'bitcoin-knots' : 8332 ,
'electrs' : 50001 ,
'btcpay-server' : 23000 ,
'lnd' : 8080 ,
'mempool' : 4080 ,
'homeassistant' : 8123 ,
'grafana' : 3000 ,
'searxng' : 8888 ,
'ollama' : 11434 ,
'nextcloud' : 8082 ,
'vaultwarden' : 8222 ,
'jellyfin' : 8096 ,
'photoprism' : 2342 ,
'immich' : 2283 ,
'portainer' : 9443 ,
'uptime-kuma' : 3001 ,
'tailscale' : 41641 ,
2026-03-18 19:24:52 +00:00
'fedimint' : 8175 ,
'thunderhub' : 3010 ,
2026-03-09 17:09:59 +00:00
'nostr-rs-relay' : 7000 ,
'syncthing' : 8384 ,
'penpot' : 9001 ,
'nginx-proxy-manager' : 8181 ,
'indeedhub' : 8190 ,
'dwn' : 3000 ,
'tor' : 9050 ,
2026-01-24 22:59:20 +00:00
}
2026-03-09 17:09:59 +00:00
// Auto-assign port for unknown apps (start at 8200, increment)
let nextAutoPort = 8200
2026-01-27 23:06:18 +00:00
// Helper: Query real Docker containers
async function getDockerContainers ( ) {
2026-03-18 21:06:14 +00:00
if ( ! docker ) return { }
2026-01-27 23:06:18 +00:00
try {
const containers = await docker . listContainers ( { all : true } )
// Map of container names to app IDs
const containerMapping = {
'archy-bitcoin' : 'bitcoin' ,
'archy-btcpay' : 'btcpay-server' ,
'archy-homeassistant' : 'homeassistant' ,
'archy-grafana' : 'grafana' ,
'archy-endurain' : 'endurain' ,
'archy-fedimint' : 'fedimint' ,
'archy-morphos' : 'morphos-server' ,
'archy-lnd' : 'lightning-stack' ,
'archy-mempool-web' : 'mempool' ,
2026-02-17 15:03:34 +00:00
'mempool-electrs' : 'mempool-electrs' ,
2026-01-27 23:06:18 +00:00
'archy-ollama' : 'ollama' ,
'archy-searxng' : 'searxng' ,
'archy-penpot-frontend' : 'penpot'
}
const apps = { }
for ( const container of containers ) {
const name = container . Names [ 0 ] . replace ( /^\// , '' )
const appId = containerMapping [ name ]
if ( ! appId ) continue
const isRunning = container . State === 'running'
const ports = container . Ports || [ ]
const hostPort = ports . find ( p => p . PublicPort ) ? . PublicPort || null
// Get app metadata
const appMetadata = {
'bitcoin' : {
title : 'Bitcoin Core' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/bitcoin-core.png' ,
2026-01-27 23:06:18 +00:00
description : 'Full Bitcoin node implementation'
} ,
'btcpay-server' : {
title : 'BTCPay Server' ,
icon : '/assets/img/app-icons/btcpay-server.png' ,
description : 'Self-hosted Bitcoin payment processor'
} ,
'homeassistant' : {
title : 'Home Assistant' ,
icon : '/assets/img/app-icons/homeassistant.png' ,
description : 'Open source home automation platform'
} ,
'grafana' : {
title : 'Grafana' ,
icon : '/assets/img/grafana.png' ,
description : 'Analytics and monitoring platform'
} ,
'endurain' : {
title : 'Endurain' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/endurain.png' ,
2026-01-27 23:06:18 +00:00
description : 'Application platform'
} ,
'fedimint' : {
title : 'Fedimint' ,
2026-02-17 15:03:34 +00:00
icon : '/assets/img/app-icons/fedimint.png' ,
2026-01-27 23:06:18 +00:00
description : 'Federated Bitcoin mint'
} ,
'morphos-server' : {
title : 'MorphOS Server' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/morphos.png' ,
2026-01-27 23:06:18 +00:00
description : 'Server platform'
} ,
'lightning-stack' : {
title : 'Lightning Stack' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/lnd.png' ,
2026-01-27 23:06:18 +00:00
description : 'Lightning Network (LND)'
} ,
'mempool' : {
title : 'Mempool' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/mempool.webp' ,
2026-01-27 23:06:18 +00:00
description : 'Bitcoin blockchain explorer'
} ,
2026-02-17 15:03:34 +00:00
'mempool-electrs' : {
title : 'Electrs' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/electrumx.webp' ,
2026-02-17 15:03:34 +00:00
description : 'Electrum protocol indexer for Bitcoin'
} ,
2026-01-27 23:06:18 +00:00
'ollama' : {
title : 'Ollama' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/ollama.png' ,
2026-01-27 23:06:18 +00:00
description : 'Run large language models locally'
} ,
'searxng' : {
title : 'SearXNG' ,
icon : '/assets/img/app-icons/searxng.png' ,
description : 'Privacy-respecting metasearch engine'
} ,
'penpot' : {
title : 'Penpot' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/archipelago-a.svg' ,
2026-01-27 23:06:18 +00:00
description : 'Open-source design and prototyping'
}
}
const metadata = appMetadata [ appId ] || {
title : appId ,
2026-03-06 01:11:00 +00:00
icon : '/assets/icon/pwa-192x192-v2.png' ,
2026-01-27 23:06:18 +00:00
description : ` ${ appId } application `
}
apps [ appId ] = {
title : metadata . title ,
version : '1.0.0' ,
status : isRunning ? 'running' : 'stopped' ,
state : isRunning ? 'running' : 'stopped' ,
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
health : isRunning ? 'healthy' : null ,
2026-01-27 23:06:18 +00:00
'static-files' : {
license : 'MIT' ,
instructions : metadata . description ,
icon : metadata . icon
} ,
manifest : {
id : appId ,
title : metadata . title ,
version : '1.0.0' ,
description : {
short : metadata . description ,
long : metadata . description
} ,
'release-notes' : 'Initial release' ,
license : 'MIT' ,
'wrapper-repo' : '#' ,
'upstream-repo' : '#' ,
'support-site' : '#' ,
'marketing-site' : '#' ,
'donation-url' : null ,
interfaces : hostPort ? {
main : {
name : 'Web Interface' ,
description : ` ${ metadata . title } web interface ` ,
ui : true
}
} : { }
} ,
installed : {
'current-dependents' : { } ,
'current-dependencies' : { } ,
'last-backup' : null ,
'interface-addresses' : hostPort ? {
main : {
'tor-address' : ` ${ appId } .onion ` ,
'lan-address' : ` http://localhost: ${ hostPort } `
}
} : { } ,
status : isRunning ? 'running' : 'stopped'
}
}
}
return apps
} catch ( error ) {
console . error ( '[Docker] Error querying containers:' , error . message )
return { }
}
}
2026-01-24 22:59:20 +00:00
// Helper: Check if Docker/Podman is available
async function isContainerRuntimeAvailable ( ) {
try {
// Try Podman first (Archipelago's choice)
await execPromise ( 'podman ps' )
return { available : true , runtime : 'podman' }
} catch {
try {
// Fallback to Docker
await execPromise ( 'docker ps' )
return { available : true , runtime : 'docker' }
} catch {
return { available : false , runtime : null }
}
}
}
2026-03-09 17:09:59 +00:00
// 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' } ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
'electrs' : { title : 'Electrs' , shortDesc : 'Electrum protocol indexer for Bitcoin' , icon : '/assets/img/app-icons/electrumx.webp' } ,
2026-03-09 17:09:59 +00:00
'btcpay-server' : { title : 'BTCPay Server' , shortDesc : 'Self-hosted Bitcoin payment processor' , icon : '/assets/img/app-icons/btcpay-server.png' } ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
'lnd' : { title : 'LND' , shortDesc : 'Lightning Network Daemon' , icon : '/assets/img/app-icons/lnd.png' } ,
2026-03-09 17:09:59 +00:00
'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' } ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
'penpot' : { title : 'Penpot' , shortDesc : 'Open-source design and prototyping platform' , icon : '/assets/img/app-icons/archipelago-a.svg' } ,
2026-03-09 17:09:59 +00:00
'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' } ,
2026-03-13 23:40:29 +00:00
'photoprism' : { title : 'PhotoPrism' , shortDesc : 'AI-powered photo management' , icon : '/assets/img/app-icons/photoprism.svg' } ,
2026-03-09 17:09:59 +00:00
'immich' : { title : 'Immich' , shortDesc : 'High-performance photo and video backup' , icon : '/assets/img/app-icons/immich.png' } ,
'filebrowser' : { title : 'File Browser' , shortDesc : 'Web-based file manager' , icon : '/assets/img/app-icons/file-browser.webp' } ,
'nginx-proxy-manager' : { title : 'Nginx Proxy Manager' , shortDesc : 'Easy proxy management with SSL' , icon : '/assets/img/app-icons/nginx.svg' } ,
'portainer' : { title : 'Portainer' , shortDesc : 'Container management UI' , icon : '/assets/img/app-icons/portainer.webp' } ,
'uptime-kuma' : { title : 'Uptime Kuma' , shortDesc : 'Self-hosted monitoring tool' , icon : '/assets/img/app-icons/uptime-kuma.webp' } ,
'tailscale' : { title : 'Tailscale' , shortDesc : 'Zero-config VPN for secure remote access' , icon : '/assets/img/app-icons/tailscale.webp' } ,
'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' } ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
'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' } ,
2026-03-18 19:24:52 +00:00
'thunderhub' : { title : 'ThunderHub' , shortDesc : 'Lightning node management UI with channel management and payments' , icon : '/assets/img/app-icons/thunderhub.svg' } ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
'tor' : { title : 'Tor' , shortDesc : 'Anonymous communication over the Tor network' , icon : '/assets/img/app-icons/tor.svg' } ,
2026-03-09 17:09:59 +00:00
'amin' : { title : 'Amin' , shortDesc : 'Administrative interface for Archipelago' , icon : '/assets/icon/pwa-192x192-v2.png' } ,
}
2026-01-24 22:59:20 +00:00
// Helper: Install package with container runtime (if available) or simulate
2026-03-09 17:09:59 +00:00
async function installPackage ( id , manifestUrl , opts = { } ) {
2026-01-24 22:59:20 +00:00
console . log ( ` [Package] 📦 Installing ${ id } ... ` )
2026-03-09 17:09:59 +00:00
2026-01-24 22:59:20 +00:00
try {
// Check if already installed
if ( mockData [ 'package-data' ] [ id ] ) {
throw new Error ( ` Package ${ id } is already installed ` )
}
2026-03-09 17:09:59 +00:00
const version = opts . version || '0.1.0'
2026-01-24 22:59:20 +00:00
const runtime = await isContainerRuntimeAvailable ( )
2026-03-06 01:11:00 +00:00
2026-03-09 17:09:59 +00:00
// Get package metadata from marketplace lookup, then fallback
const metadata = marketplaceMetadata [ id ] || {
title : id . split ( '-' ) . map ( w => w . charAt ( 0 ) . toUpperCase ( ) + w . slice ( 1 ) ) . join ( ' ' ) ,
2026-01-24 22:59:20 +00:00
shortDesc : ` ${ id } application ` ,
2026-03-09 17:09:59 +00:00
icon : ` /assets/img/app-icons/ ${ id } .png `
}
// Determine port — use known mapping, or auto-assign a unique one
let assignedPort = portMappings [ id ]
if ( ! assignedPort ) {
while ( usedPorts . has ( nextAutoPort ) ) nextAutoPort ++
assignedPort = nextAutoPort ++
2026-01-24 22:59:20 +00:00
}
usedPorts . add ( assignedPort )
let containerMode = false
let actuallyRunning = false
// Try to run with container runtime if available
if ( runtime . available ) {
try {
console . log ( ` [Package] 🐳 ${ runtime . runtime } available, attempting to run container... ` )
const containerName = ` ${ id } -archipelago `
const stopCmd = runtime . runtime === 'podman'
? ` podman stop ${ containerName } 2>/dev/null || true `
: ` docker stop ${ containerName } 2>/dev/null || true `
const rmCmd = runtime . runtime === 'podman'
? ` podman rm ${ containerName } 2>/dev/null || true `
: ` docker rm ${ containerName } 2>/dev/null || true `
// Stop and remove existing container if it exists
await execPromise ( stopCmd )
await execPromise ( rmCmd )
// Check if image exists
const imageCheckCmd = runtime . runtime === 'podman'
? ` podman images -q ${ id } : ${ version } `
: ` docker images -q ${ id } : ${ version } `
let { stdout } = await execPromise ( imageCheckCmd )
if ( stdout . trim ( ) ) {
// Image exists, start container
const runCmd = runtime . runtime === 'podman'
? ` podman run -d --name ${ containerName } -p ${ assignedPort } :80 ${ id } : ${ version } `
: ` docker run -d --name ${ containerName } -p ${ assignedPort } :80 ${ id } : ${ version } `
await execPromise ( runCmd )
// Wait for container to be ready
await new Promise ( resolve => setTimeout ( resolve , 2000 ) )
// Verify container is running
const statusCmd = runtime . runtime === 'podman'
? ` podman ps --filter name= ${ containerName } --format "{{.Status}}" `
: ` docker ps --filter name= ${ containerName } --format "{{.Status}}" `
const { stdout : containerStatus } = await execPromise ( statusCmd )
if ( containerStatus . includes ( 'Up' ) ) {
containerMode = true
actuallyRunning = true
runningContainers . set ( id , {
port : assignedPort ,
containerId : containerName ,
runtime : runtime . runtime
} )
console . log ( ` [Package] 🐳 ${ runtime . runtime } container running on port ${ assignedPort } ` )
}
} else {
console . log ( ` [Package] ℹ ️ Container image ${ id } : ${ version } not found, using simulation mode ` )
}
} catch ( containerError ) {
console . log ( ` [Package] ⚠️ Container error ( ${ containerError . message } ), falling back to simulation ` )
}
} else {
console . log ( ` [Package] ℹ ️ Container runtime not available, using simulation mode ` )
}
// If container didn't work, simulate installation
if ( ! containerMode ) {
await new Promise ( resolve => setTimeout ( resolve , 1500 ) )
runningContainers . set ( id , { port : assignedPort , containerId : null , runtime : null } )
}
2026-03-09 17:09:59 +00:00
// Add to mock data using staticApp format for consistency
2026-01-24 22:59:20 +00:00
mockData [ 'package-data' ] [ id ] = {
2026-03-09 17:09:59 +00:00
... staticApp ( {
id ,
title : metadata . title ,
version ,
shortDesc : metadata . shortDesc ,
longDesc : metadata . shortDesc ,
state : 'running' ,
lanPort : assignedPort ,
icon : metadata . icon ,
} ) ,
2026-01-24 22:59:20 +00:00
port : assignedPort ,
containerMode : containerMode ,
actuallyRunning : actuallyRunning ,
}
// Broadcast update
broadcastUpdate ( [
{
op : 'add' ,
path : ` /package-data/ ${ id } ` ,
value : mockData [ 'package-data' ] [ id ]
}
] )
if ( containerMode ) {
console . log ( ` [Package] ✅ ${ id } installed and RUNNING at http://localhost: ${ assignedPort } ` )
} else {
console . log ( ` [Package] ✅ ${ id } installed (simulated) ` )
}
return { success : true , port : assignedPort , containerMode }
} catch ( error ) {
console . error ( ` [Package] ❌ Installation failed: ` , error . message )
throw error
}
}
// Helper: Uninstall package
async function uninstallPackage ( id ) {
console . log ( ` [Package] 🗑️ Uninstalling ${ id } ... ` )
try {
2026-02-18 08:30:12 +00:00
if ( staticDevApps [ id ] ) {
throw new Error ( ` ${ id } is a demo app and cannot be uninstalled ` )
}
2026-01-24 22:59:20 +00:00
if ( ! mockData [ 'package-data' ] [ id ] ) {
throw new Error ( ` Package ${ id } is not installed ` )
}
// Stop container if it's running
const containerInfo = runningContainers . get ( id )
if ( containerInfo && containerInfo . containerId ) {
try {
const runtime = containerInfo . runtime || 'docker'
const stopCmd = runtime === 'podman'
? ` podman stop ${ containerInfo . containerId } 2>/dev/null || true `
: ` docker stop ${ containerInfo . containerId } 2>/dev/null || true `
const rmCmd = runtime === 'podman'
? ` podman rm ${ containerInfo . containerId } 2>/dev/null || true `
: ` docker rm ${ containerInfo . containerId } 2>/dev/null || true `
console . log ( ` [Package] 🐳 Stopping container ${ containerInfo . containerId } ... ` )
await execPromise ( stopCmd )
await execPromise ( rmCmd )
console . log ( ` [Package] 🐳 Container stopped ` )
} catch ( error ) {
console . log ( ` [Package] ⚠️ Error stopping container: ${ error . message } ` )
}
}
await new Promise ( resolve => setTimeout ( resolve , 1000 ) )
const port = mockData [ 'package-data' ] [ id ] . port
if ( port ) {
usedPorts . delete ( port )
}
runningContainers . delete ( id )
delete mockData [ 'package-data' ] [ id ]
broadcastUpdate ( [
{
op : 'remove' ,
path : ` /package-data/ ${ id } `
}
] )
console . log ( ` [Package] ✅ ${ id } uninstalled successfully ` )
return { success : true }
} catch ( error ) {
console . error ( ` [Package] ❌ Uninstall failed: ` , error . message )
throw error
}
}
// Mock data
2026-06-22 09:28:05 -04:00
const SEED _MOCKDATA = {
2026-01-24 22:59:20 +00:00
'server-info' : {
2026-03-09 13:03:53 +00:00
id : 'archipelago-demo' ,
2026-06-22 09:28:05 -04:00
version : APP _VERSION ,
2026-03-09 13:03:53 +00:00
name : 'Archipelago' ,
pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
2026-01-24 22:59:20 +00:00
'status-info' : {
restarting : false ,
'shutting-down' : false ,
updated : false ,
'backup-progress' : null ,
'update-progress' : null ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
// The mock has no container scanner — report the scan as done so app
// cards never sit on "Checking..." in demo/dev.
'containers-scanned' : true ,
2026-01-24 22:59:20 +00:00
} ,
2026-03-09 18:49:20 +00:00
'lan-address' : 'localhost' ,
2026-03-09 13:03:53 +00:00
'tor-address' : 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion' ,
unread : 3 ,
'wifi-ssids' : [ 'Home-5G' , 'Archipelago-Mesh' , 'Neighbors-Open' ] ,
'zram-enabled' : true ,
2026-01-24 22:59:20 +00:00
} ,
2026-03-09 13:03:53 +00:00
'package-data' : { } , // Will be populated from Docker + static apps
2026-01-24 22:59:20 +00:00
ui : {
name : 'Archipelago' ,
'ack-welcome' : '0.1.0' ,
marketplace : {
'selected-hosts' : [ ] ,
'known-hosts' : { } ,
} ,
theme : 'dark' ,
} ,
}
2026-03-09 13:03:53 +00:00
// 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 ,
2026-02-18 08:30:12 +00:00
'static-files' : {
2026-03-09 13:03:53 +00:00
license : license || 'MIT' ,
instructions : shortDesc ,
icon : icon || ` /assets/img/app-icons/ ${ id } .png ` ,
2026-02-18 08:30:12 +00:00
} ,
manifest : {
2026-03-09 13:03:53 +00:00
id ,
title ,
version ,
description : { short : shortDesc , long : longDesc || shortDesc } ,
'release-notes' : 'Latest stable release' ,
license : license || 'MIT' ,
2026-02-18 08:30:12 +00:00
'wrapper-repo' : '#' ,
'upstream-repo' : '#' ,
'support-site' : '#' ,
'marketing-site' : '#' ,
'donation-url' : null ,
interfaces : {
2026-03-09 13:03:53 +00:00
main : { name : 'Web Interface' , description : ` ${ title } web interface ` , ui : true } ,
} ,
2026-02-18 08:30:12 +00:00
} ,
installed : {
'current-dependents' : { } ,
'current-dependencies' : { } ,
'last-backup' : null ,
'interface-addresses' : {
main : {
2026-03-09 13:03:53 +00:00
'tor-address' : torHost ? ` ${ torHost } .onion ` : ` ${ id } .onion ` ,
2026-03-09 18:49:20 +00:00
'lan-address' : lanPort ? ` http://localhost: ${ lanPort } ` : '' ,
2026-03-09 13:03:53 +00:00
} ,
2026-02-18 08:30:12 +00:00
} ,
2026-03-09 13:03:53 +00:00
status : state ,
} ,
2026-02-18 08:30:12 +00:00
}
}
2026-06-22 12:39:33 -04:00
// Static dev apps (always shown in My Apps when using mock backend).
2026-03-09 13:03:53 +00:00
const staticDevApps = {
bitcoin : staticApp ( {
id : 'bitcoin' ,
title : 'Bitcoin Core' ,
2026-06-22 13:55:50 -04:00
version : '28.4.0' ,
2026-03-09 13:03:53 +00:00
shortDesc : 'Full Bitcoin node' ,
longDesc : 'Validate every transaction and block. Full consensus enforcement — the bedrock of sovereignty.' ,
state : 'running' ,
lanPort : 8332 ,
2026-06-22 12:39:33 -04:00
icon : '/assets/img/app-icons/bitcoin-core.png' ,
2026-03-09 13:03:53 +00:00
} ) ,
2026-06-22 13:55:50 -04:00
'bitcoin-knots' : staticApp ( {
id : 'bitcoin-knots' ,
title : 'Bitcoin Knots' ,
version : '28.1.0' ,
shortDesc : 'Full Bitcoin node' ,
longDesc : 'Validate and relay Bitcoin blocks and transactions with the Archipelago custom node UI.' ,
state : 'running' ,
lanPort : 8334 ,
icon : '/assets/img/app-icons/bitcoin-knots.webp' ,
} ) ,
2026-03-09 13:03:53 +00:00
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 ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/lnd.png' ,
2026-03-09 13:03:53 +00:00
} ) ,
2026-06-22 13:55:50 -04:00
electrumx : staticApp ( {
id : 'electrumx' ,
title : 'ElectrumX' ,
version : '1.18.0' ,
shortDesc : 'Electrum server' ,
2026-03-09 13:03:53 +00:00
longDesc : 'Private blockchain indexing for wallet lookups. Connect Sparrow, BlueWallet, or any Electrum-compatible wallet.' ,
state : 'running' ,
2026-06-22 13:55:50 -04:00
lanPort : 50002 ,
icon : '/assets/img/app-icons/electrumx.webp' ,
2026-03-09 13:03:53 +00:00
} ) ,
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 ,
2026-06-22 12:39:33 -04:00
icon : '/assets/img/app-icons/mempool.webp' ,
2026-03-09 13:03:53 +00:00
} ) ,
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 ,
} ) ,
2026-06-11 00:24:40 -04:00
meshtastic : staticApp ( {
id : 'meshtastic' ,
title : 'Meshtastic' ,
version : '2-daily-alpine' ,
shortDesc : 'LoRa mesh networking' ,
longDesc : 'Open-source mesh networking for LoRa radios. Create decentralized communication networks.' ,
state : 'running' ,
lanPort : 4403 ,
icon : '/assets/img/app-icons/meshcore.svg' ,
} ) ,
2026-03-09 17:09:59 +00:00
filebrowser : staticApp ( {
id : 'filebrowser' ,
title : 'File Browser' ,
version : '2.27.0' ,
shortDesc : 'Web-based file manager' ,
longDesc : 'Browse, upload, and manage files through an elegant web interface. Drag-and-drop uploads, media previews, and sharing.' ,
state : 'running' ,
lanPort : 8083 ,
icon : '/assets/img/app-icons/file-browser.webp' ,
} ) ,
2026-03-18 19:24:52 +00:00
fedimint : staticApp ( {
id : 'fedimint' ,
title : 'Fedimint' ,
version : '0.10.0' ,
shortDesc : 'Federated Bitcoin mint' ,
longDesc : 'Federated Chaumian e-cash mint with Guardian UI. Community custody, private payments, and Lightning gateways.' ,
state : 'running' ,
lanPort : 8175 ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
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' ,
2026-03-18 19:24:52 +00:00
} ) ,
2026-03-09 13:03:53 +00:00
}
2026-02-18 08:30:12 +00:00
function mergePackageData ( dockerApps ) {
return { ... dockerApps , ... staticDevApps }
}
2026-01-27 23:06:18 +00:00
// Initialize package data from Docker on startup
async function initializePackageData ( ) {
console . log ( '[Docker] Querying running containers...' )
const dockerApps = await getDockerContainers ( )
2026-02-18 08:30:12 +00:00
mockData [ 'package-data' ] = mergePackageData ( dockerApps )
2026-01-27 23:06:18 +00:00
2026-02-18 08:30:12 +00:00
const appCount = Object . keys ( mockData [ 'package-data' ] ) . length
const runningCount = Object . values ( mockData [ 'package-data' ] ) . filter ( app => app . state === 'running' ) . length
2026-01-27 23:06:18 +00:00
console . log ( ` [Docker] Found ${ appCount } containers ( ${ runningCount } running) ` )
if ( appCount > 0 ) {
console . log ( '[Docker] Apps detected:' )
2026-02-18 08:30:12 +00:00
Object . entries ( mockData [ 'package-data' ] ) . forEach ( ( [ id , app ] ) => {
2026-01-27 23:06:18 +00:00
const port = app . installed ? . [ 'interface-addresses' ] ? . main ? . [ 'lan-address' ]
console . log ( ` - ${ app . title } ( ${ app . state } ) ${ port ? ` → ${ port } ` : '' } ` )
} )
} else {
console . log ( '[Docker] No containers found. Start docker-compose to see apps.' )
}
}
2026-01-24 22:59:20 +00:00
// Handle CORS preflight
app . options ( '/rpc/v1' , ( req , res ) => {
res . status ( 200 ) . end ( )
} )
2026-06-11 00:24:40 -04:00
app . get ( '/' , ( _req , res ) => {
const uiPort = process . env . VITE _DEV _SERVER _PORT || '8102'
res
. status ( 200 )
. type ( 'html' )
. send ( ` <!doctype html>
< html >
< head >
< meta charset = "utf-8" >
< meta http - equiv = "refresh" content = "0; url=http://localhost:${uiPort}/" >
< title > Archipelago dev backend < / t i t l e >
< / h e a d >
< body >
< p > This is the mock JSON - RPC backend . Open the dashboard at
< a href = "http://localhost:${uiPort}/" > http : //localhost:${uiPort}/</a>.
< / p >
< / b o d y >
< / h t m l > ` )
} )
app . get ( '/rpc/v1' , ( _req , res ) => {
const uiPort = process . env . VITE _DEV _SERVER _PORT || '8102'
res
. status ( 405 )
. type ( 'text/plain' )
. send ( ` JSON-RPC is available at /rpc/v1 for POST requests only. \n Open the dashboard at http://localhost: ${ uiPort } /. \n ` )
} )
2026-06-22 09:28:05 -04:00
// DEMO runs on a testnet (signet) so visitors can play with worthless coins.
const DEMO _CHAIN = DEMO ? 'signet' : 'main'
2026-06-11 00:24:40 -04:00
function mockBitcoinBlockchainInfo ( ) {
return {
2026-06-22 09:28:05 -04:00
chain : DEMO _CHAIN ,
2026-06-11 00:24:40 -04:00
blocks : 902418 ,
headers : 902418 ,
bestblockhash : randomHex ( 32 ) ,
difficulty : 126_984_812_384_099 . 3 ,
verificationprogress : 0.999998 ,
initialblockdownload : false ,
size _on _disk : 678_000_000_000 ,
pruned : false ,
chainwork : '00000000000000000000000000000000000000008d4b42d8b9b0000000000000' ,
}
}
function mockBitcoinNetworkInfo ( ) {
return {
version : 270100 ,
subversion : '/Satoshi:27.1/Knots:20240513/' ,
protocolversion : 70016 ,
localservices : '0000000000000409' ,
localrelay : true ,
timeoffset : 0 ,
networkactive : true ,
connections : 18 ,
networks : [
{ name : 'ipv4' , limited : false , reachable : true , proxy : '' , proxy _randomize _credentials : false } ,
{ name : 'onion' , limited : false , reachable : true , proxy : '127.0.0.1:9050' , proxy _randomize _credentials : true } ,
] ,
}
}
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
// 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 ,
}
}
2026-06-11 00:24:40 -04:00
function bitcoinRelayStatusPayload ( ) {
return {
settings : bitcoinRelayMockState . settings ,
trusted _nodes : bitcoinRelayMockState . trusted _nodes ,
requests : bitcoinRelayMockState . requests ,
local _node : {
synced : true ,
blocks : 902418 ,
headers : 902418 ,
2026-06-22 09:28:05 -04:00
chain : DEMO _CHAIN ,
2026-06-11 00:24:40 -04:00
status _ok : true ,
status _stale : false ,
error : null ,
} ,
credentials : {
username : 'txrelay' ,
available : true ,
password _available : true ,
rpcauth _available : true ,
client _env _available : true ,
client _env _path : '/var/lib/archipelago/secrets/bitcoin-rpc-txrelay-client.env' ,
restart _hint : 'If this was just generated, restart Bitcoin Core/Knots so bitcoind loads the txrelay rpcauth whitelist.' ,
} ,
}
}
app . get ( '/app/bitcoin-ui/' , async ( _req , res ) => {
try {
const html = await fs . readFile ( path . join ( _ _dirname , '..' , 'docker' , 'bitcoin-ui' , 'index.html' ) , 'utf8' )
res . type ( 'html' ) . send ( html )
} catch ( error ) {
res . status ( 500 ) . type ( 'text/plain' ) . send ( ` Unable to load Bitcoin UI mock: ${ error . message } ` )
}
} )
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
// ── Mock app UIs served in the in-app iframe (DEMO) ─────────────────────────
2026-06-22 13:55:50 -04:00
function demoAppShell ( title , sub , iconPath , bodyHtml ) {
const accent = '#f7931a' // Archipelago orange
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
return ` <!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
< meta name = "viewport" content = "width=device-width,initial-scale=1" > < title > $ { title } < / t i t l e >
< style >
* { margin : 0 ; padding : 0 ; box - sizing : border - box }
2026-06-22 13:55:50 -04:00
body { background : # 000 ; color : # f2f2f4 ; font - family : system - ui , - apple - system , Segoe UI , Roboto , sans - serif ; min - height : 100 vh ; padding : 28 px ;
background - image : radial - gradient ( 1200 px 500 px at 50 % - 10 % , rgba ( 247 , 147 , 26 , . 07 ) , transparent 70 % ) ; }
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
. wrap { max - width : 860 px ; margin : 0 auto }
. hd { display : flex ; align - items : center ; gap : 14 px ; margin - bottom : 22 px }
2026-06-22 13:55:50 -04:00
. ico { width : 46 px ; height : 46 px ; border - radius : 12 px ; object - fit : cover ; background : rgba ( 255 , 255 , 255 , . 04 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , . 08 ) ; flex - shrink : 0 }
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
h1 { font - size : 22 px ; font - weight : 650 }
2026-06-22 13:55:50 -04:00
. sub { color : # 8 a8f9a ; font - size : 13 px ; margin - top : 2 px }
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
. grid { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 200 px , 1 fr ) ) ; gap : 14 px ; margin - bottom : 18 px }
2026-06-22 13:55:50 -04:00
. card { background : rgba ( 255 , 255 , 255 , . 035 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , . 08 ) ; border - radius : 14 px ; padding : 16 px }
. k { color : # 8 a8f9a ; font - size : 12 px ; text - transform : uppercase ; letter - spacing : . 5 px }
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
. v { font - size : 20 px ; font - weight : 650 ; margin - top : 6 px }
. v . mono { font - family : ui - monospace , Menlo , monospace ; font - size : 13 px ; word - break : break - all ; font - weight : 500 }
2026-06-22 13:55:50 -04:00
. bar { height : 8 px ; border - radius : 6 px ; background : rgba ( 255 , 255 , 255 , . 07 ) ; overflow : hidden ; margin - top : 10 px }
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
. bar > i { display : block ; height : 100 % ; background : $ { accent } }
2026-06-22 13:55:50 -04:00
. badge { display : inline - flex ; align - items : center ; gap : 6 px ; background : rgba ( 34 , 197 , 94 , . 14 ) ; color : # 86 efac ; border : 1 px solid rgba ( 34 , 197 , 94 , . 3 ) ; padding : 4 px 10 px ; border - radius : 999 px ; font - size : 12 px }
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
table { width : 100 % ; border - collapse : collapse ; font - size : 13 px }
td , th { text - align : left ; padding : 9 px 6 px ; border - bottom : 1 px solid rgba ( 255 , 255 , 255 , . 06 ) }
2026-06-22 13:55:50 -04:00
th { color : # 8 a8f9a ; font - weight : 500 ; font - size : 11 px ; text - transform : uppercase }
. demo - tag { position : fixed ; bottom : 14 px ; right : 16 px ; font - size : 11 px ; color : # 5 b6070 }
< / s t y l e > < / h e a d > < b o d y > < d i v c l a s s = " w r a p " >
< div class = "hd" >
< img class = "ico" src = "${iconPath}" onerror = "this.style.visibility='hidden'" alt = "" >
< div > < h1 > $ { title } < / h 1 > < d i v c l a s s = " s u b " > $ { s u b } < / d i v > < / d i v >
< / d i v >
$ { bodyHtml }
< / d i v > < d i v c l a s s = " d e m o - t a g " > A r c h i p e l a g o d e m o · s i g n e t < / d i v > < / b o d y > < / h t m l > `
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
}
2026-07-15 10:35:42 +01:00
// ── Rich peer content library (demo) ─────────────────────────────────────────
// What a healthy decentralised network's shared catalog looks like: films,
// series, books, music, photography and documents with real metadata. Music,
// photos and documents are backed by REAL committed files (they play/open);
// films/series/books are paid so the buy-flow poster/cover previews carry the
// visuals. Preview images live in demo/peer-media (sourced from the web via
// picsum.photos — Unsplash-licensed — seeds pinned in git history).
const PEER _MEDIA = ( f ) => path . join ( _ _dirname , '..' , 'demo' , 'peer-media' , f )
const PEER _CONTENT = ( ... p ) => path . join ( _ _dirname , '..' , 'demo' , 'content' , ... p )
const PEER _LIBRARY = [
// Films & series — paid, poster previews
{ id : 'film-block-height' , filename : 'Block Height (2025) 2160p.mp4' , mime _type : 'video/mp4' , size _bytes : 4_512_374_784 , access : { paid : { price _sats : 2500 } } , preview : PEER _MEDIA ( 'poster-film-block.jpg' ) ,
description : 'Documentary · 2025 · 1h 42m · 3840× 2160 HEVC 10-bit · dir. M. Castillo — three node operators ride out a halving epoch.' } ,
{ id : 'film-the-signal' , filename : 'The Signal (2024) 1080p.mkv' , mime _type : 'video/x-matroska' , size _bytes : 2_147_483_648 , access : { paid : { price _sats : 2100 } } , preview : PEER _MEDIA ( 'poster-film-signal.jpg' ) ,
description : 'Thriller · 2024 · 2h 08m · 1920× 1080 x264 · A radio engineer discovers her mesh network is carrying more than messages.' } ,
{ id : 'film-meshwork' , filename : 'Meshwork — infrastructure for the willing (720p).mp4' , mime _type : 'video/mp4' , size _bytes : 891_289_600 , access : { paid : { price _sats : 800 } } , preview : PEER _MEDIA ( 'poster-film-mesh.jpg' ) ,
description : 'Documentary short · 2026 · 34m · 1280× 720 · Community LoRa + Reticulum builds across the highlands.' } ,
{ id : 'series-node-runners-s01e01' , filename : 'Node Runners S01E01 — Genesis.mkv' , mime _type : 'video/x-matroska' , size _bytes : 734_003_200 , access : { paid : { price _sats : 500 } } , preview : PEER _MEDIA ( 'poster-series-node.jpg' ) ,
description : 'Series · S01E01 · 43m · 1080p · Docuseries following first-time sovereign node builders.' } ,
// Books — paid, cover previews
{ id : 'book-sovereign-notes' , filename : 'The Sovereign Individual — annotated.epub' , mime _type : 'application/epub+zip' , size _bytes : 3_842_048 , access : { paid : { price _sats : 300 } } , preview : PEER _MEDIA ( 'cover-book-sovereign.jpg' ) ,
description : 'EPUB · 512 pp · community margin-notes edition, 2026 · Davidson & Rees-Mogg.' } ,
{ id : 'book-cypherpunk-essays' , filename : 'Cypherpunk Essays Vol. II.epub' , mime _type : 'application/epub+zip' , size _bytes : 2_097_152 , access : { paid : { price _sats : 210 } } , preview : PEER _MEDIA ( 'cover-book-cypher.jpg' ) ,
description : 'EPUB · 348 pp · 2025 anthology — privacy engineering, remailers, digital cash history.' } ,
{ id : 'book-broadcast-power' , filename : 'Broadcast Power — pirate radio to mesh.pdf' , mime _type : 'application/pdf' , size _bytes : 18_874_368 , access : { paid : { price _sats : 150 } } , preview : PEER _MEDIA ( 'cover-book-broadcast.jpg' ) ,
description : 'PDF · 214 pp · illustrated 2024 history of community-run transmission networks.' } ,
// Music — free, streams the real committed files
{ id : 'song-leaves-brown' , filename : 'All the leaves are brown.mp3' , mime _type : 'audio/mpeg' , size _bytes : 2_936_012 , access : 'free' , disk : PEER _CONTENT ( 'music' , 'All the leaves are brown.mp3' ) ,
description : 'MP3 320 kbps · Archy Sessions, 2026 · folk cover, one-take living-room recording.' } ,
{ id : 'song-exploited-substrate' , filename : 'An Exploited Substrate.mp3' , mime _type : 'audio/mpeg' , size _bytes : 4_194_304 , access : 'free' , disk : PEER _CONTENT ( 'music' , 'An Exploited Substrate.mp3' ) ,
description : 'MP3 320 kbps · Node Noise EP, 2025 · instrumental synthwave.' } ,
{ id : 'song-architects' , filename : 'Architects of Tomorrow.wav' , mime _type : 'audio/wav' , size _bytes : 34_734_080 , access : 'free' , disk : PEER _CONTENT ( 'music' , 'Architects of Tomorrow.wav' ) ,
description : 'WAV 16-bit/44.1 kHz lossless master · Node Noise EP, 2025.' } ,
{ id : 'song-bitcoin-salad' , filename : 'Bitcoin Salad.MP3' , mime _type : 'audio/mpeg' , size _bytes : 3_984_588 , access : 'free' , disk : PEER _CONTENT ( 'music' , 'Bitcoin Salad.MP3' ) ,
description : 'MP3 256 kbps · Kitchen Mix Vol. 3, 2024 · novelty electro.' } ,
{ id : 'song-builders' , filename : 'Builders, not talkers (Remastered).mp3' , mime _type : 'audio/mpeg' , size _bytes : 4_508_876 , access : 'free' , disk : PEER _CONTENT ( 'music' , 'Builders, not talkers (Remastered).mp3' ) ,
description : 'MP3 320 kbps · 2026 remaster · hip-hop instrumental.' } ,
// Photography — free, the committed JPEGs themselves (preview = full image)
{ id : 'photo-aurora-fjord' , filename : 'aurora-over-the-fjord.jpg' , mime _type : 'image/jpeg' , size _bytes : 79_422 , access : 'free' , disk : PEER _MEDIA ( 'photo-aurora-fjord.jpg' ) ,
description : '800× 533 · f/1.8 · 8s · ISO 1600 · Sony A7 IV 24mm · Lofoten, Norway · free license, sourced via picsum.photos.' } ,
{ id : 'photo-mountain-lake' , filename : 'mountain-lake-still.jpg' , mime _type : 'image/jpeg' , size _bytes : 29_200 , access : 'free' , disk : PEER _MEDIA ( 'photo-mountain-lake.jpg' ) ,
description : '800× 533 · f/8 · 1/250s · ISO 100 · Fuji X-T5 23mm · Dolomites, Italy · free license, sourced via picsum.photos.' } ,
{ id : 'photo-city-neon' , filename : 'city-neon-rain.jpg' , mime _type : 'image/jpeg' , size _bytes : 59_855 , access : 'free' , disk : PEER _MEDIA ( 'photo-city-neon.jpg' ) ,
description : '800× 533 · f/1.4 · 1/60s · ISO 800 · Leica Q3 28mm · Osaka, Japan · free license, sourced via picsum.photos.' } ,
{ id : 'photo-desert-dunes' , filename : 'desert-dunes-dawn.jpg' , mime _type : 'image/jpeg' , size _bytes : 24_646 , access : 'free' , disk : PEER _MEDIA ( 'photo-desert-dunes.jpg' ) ,
description : '800× 533 · f/11 · 1/125s · ISO 64 · Nikon Z7 II 70-200mm · Erg Chebbi, Morocco · free license, sourced via picsum.photos.' } ,
{ id : 'photo-forest-mist' , filename : 'forest-mist-morning.jpg' , mime _type : 'image/jpeg' , size _bytes : 48_504 , access : 'free' , disk : PEER _MEDIA ( 'photo-forest-mist.jpg' ) ,
description : '800× 533 · f/4 · 1/200s · ISO 400 · Canon R5 85mm · Black Forest, Germany · free license, sourced via picsum.photos.' } ,
{ id : 'photo-ocean-cliff' , filename : 'ocean-cliff-long-exposure.jpg' , mime _type : 'image/jpeg' , size _bytes : 30_517 , access : 'free' , disk : PEER _MEDIA ( 'photo-ocean-cliff.jpg' ) ,
description : '800× 533 · f/16 · 30s ND1000 · ISO 50 · Sony A7R V 16-35mm · Moher, Ireland · free license, sourced via picsum.photos.' } ,
{ id : 'photo-northern-road' , filename : 'northern-road-solitude.jpg' , mime _type : 'image/jpeg' , size _bytes : 72_053 , access : 'free' , disk : PEER _MEDIA ( 'photo-northern-road.jpg' ) ,
description : '800× 533 · f/5.6 · 1/500s · ISO 200 · drone DJI Mavic 3 · Iceland Ring Road · free license, sourced via picsum.photos.' } ,
{ id : 'photo-autumn-valley' , filename : 'autumn-valley-patchwork.jpg' , mime _type : 'image/jpeg' , size _bytes : 51_148 , access : 'free' , disk : PEER _MEDIA ( 'photo-autumn-valley.jpg' ) ,
description : '800× 533 · f/9 · 1/160s · ISO 100 · Fuji GFX 100S 110mm · Vermont, USA · free license, sourced via picsum.photos.' } ,
{ id : 'photo-harbor-dawn' , filename : 'harbor-dawn-fishing-boats.jpg' , mime _type : 'image/jpeg' , size _bytes : 36_271 , access : 'free' , disk : PEER _MEDIA ( 'photo-harbor-dawn.jpg' ) ,
description : '800× 533 · f/2.8 · 1/80s · ISO 320 · Leica M11 35mm · Cornwall, UK · free license, sourced via picsum.photos.' } ,
{ id : 'photo-alpine-ridge' , filename : 'alpine-ridge-hiker.jpg' , mime _type : 'image/jpeg' , size _bytes : 91_938 , access : 'free' , disk : PEER _MEDIA ( 'photo-alpine-ridge.jpg' ) ,
description : '800× 533 · f/7.1 · 1/320s · ISO 100 · Canon R6 II 24-70mm · Bernese Oberland, Switzerland · free license, sourced via picsum.photos.' } ,
// Documents — free, real committed files
{ id : 'doc-whitepaper-notes' , filename : 'bitcoin-whitepaper-notes.md' , mime _type : 'text/markdown' , size _bytes : 8_192 , access : 'free' , disk : PEER _CONTENT ( 'documents' , 'bitcoin-whitepaper-notes.md' ) ,
description : 'Markdown study notes on the Bitcoin whitepaper, section by section.' } ,
{ id : 'doc-sovereignty-manifesto' , filename : 'sovereignty-manifesto.txt' , mime _type : 'text/plain' , size _bytes : 4_096 , access : 'free' , disk : PEER _CONTENT ( 'documents' , 'sovereignty-manifesto.txt' ) ,
description : 'Plain-text manifesto on self-hosted infrastructure.' } ,
{ id : 'doc-node-checklist' , filename : 'node-setup-checklist.md' , mime _type : 'text/markdown' , size _bytes : 6_144 , access : 'free' , disk : PEER _CONTENT ( 'documents' , 'node-setup-checklist.md' ) ,
description : 'Step-by-step hardening checklist for a fresh Archipelago node.' } ,
{ id : 'doc-lightning-csv' , filename : 'lightning-channels.csv' , mime _type : 'text/csv' , size _bytes : 2_048 , access : 'free' , disk : PEER _CONTENT ( 'documents' , 'lightning-channels.csv' ) ,
description : 'CSV export — public channel set with capacities and fee policies.' } ,
// Software
{ id : 'sw-archy-iso' , filename : 'archipelago-1.7.99-amd64.iso' , mime _type : 'application/x-iso9660-image' , size _bytes : 2_684_354_560 , access : 'free' ,
description : 'Archipelago Node OS installer ISO · amd64 · SHA256 in release notes.' } ,
]
/ * * D e t e r m i n i s t i c p e r - p e e r s l i c e o f t h e l i b r a r y — e v e r y p e e r s h a r e s a d i f f e r e n t
* handful , popular items appear on more than one node ( it ' s a network ) . * /
function peerCatalogFor ( onion ) {
let seed = 0
for ( const c of String ( onion ) ) seed = ( seed * 31 + c . charCodeAt ( 0 ) ) >>> 0
const slot = seed % 12
return PEER _LIBRARY
. map ( ( item , i ) => ( { item , i } ) )
. filter ( ( { i } ) => i % 12 === slot || ( ( seed ^ ( i * 2654435761 ) ) >>> 0 ) % 12 < 3 )
. map ( ( { item } ) => ( {
id : item . id ,
filename : item . filename ,
mime _type : item . mime _type ,
size _bytes : item . size _bytes ,
description : item . description ,
access : item . access ,
} ) )
}
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
// 12 trusted/federated nodes for the demo Federation view.
function demoFederationNodes ( ) {
const names = [
[ 'archy-198' , 'trusted' , [ 'bitcoin-knots' , 'lnd' , 'mempool' , 'electrs' ] ] ,
[ 'arch-tailscale-1' , 'trusted' , [ 'bitcoin-knots' , 'lnd' , 'nextcloud' ] ] ,
[ 'bunker-alpha' , 'trusted' , [ 'bitcoin-knots' , 'vaultwarden' ] ] ,
[ 'seaside-node' , 'trusted' , [ 'bitcoin-knots' , 'lnd' , 'fedimint' ] ] ,
[ 'highland-relay' , 'trusted' , [ 'bitcoin-knots' , 'electrs' , 'mempool' ] ] ,
[ 'orchard-12' , 'trusted' , [ 'bitcoin-knots' , 'immich' ] ] ,
[ 'nimbus-vps' , 'trusted' , [ 'bitcoin-knots' , 'lnd' , 'thunderhub' ] ] ,
[ 'rivertown' , 'trusted' , [ 'bitcoin-knots' , 'jellyfin' ] ] ,
[ 'granite-pi' , 'trusted' , [ 'bitcoin-knots' , 'lnd' ] ] ,
[ 'saltmarsh' , 'observer' , [ 'bitcoin-knots' ] ] ,
[ 'dustbowl-node' , 'observer' , [ 'bitcoin-knots' , 'electrs' ] ] ,
[ 'frontier-7' , 'observer' , [ 'bitcoin-knots' , 'mempool' ] ] ,
]
return names . map ( ( [ name , trust , apps ] , i ) => {
const seenAgo = 60000 + i * 95000
return {
did : ` did:key:z6Mk ${ randomHex ( 20 ) } ` ,
pubkey : randomHex ( 32 ) ,
onion : ` ${ name . replace ( /[^a-z0-9]/g , '' ) } ${ randomHex ( 8 ) } .onion ` ,
trust _level : trust ,
added _at : new Date ( Date . now ( ) - ( i + 2 ) * 86400000 ) . toISOString ( ) ,
name ,
last _seen : new Date ( Date . now ( ) - seenAgo ) . toISOString ( ) ,
last _state : {
timestamp : new Date ( Date . now ( ) - seenAgo ) . toISOString ( ) ,
apps : apps . map ( id => ( { id , status : 'running' , version : '1.0' } ) ) ,
cpu _usage _percent : Math . round ( ( 6 + ( i * 7 ) % 55 ) * 10 ) / 10 ,
mem _used _bytes : ( 2 + ( i % 6 ) ) * 1_000_000_000 ,
mem _total _bytes : ( i % 2 ? 32 : 16 ) * 1_000_000_000 ,
disk _used _bytes : ( 400 + i * 90 ) * 1_000_000_000 ,
disk _total _bytes : ( i % 2 ? 2000 : 1000 ) * 1_000_000_000 ,
uptime _secs : 86400 * ( i + 1 ) ,
tor _active : trust === 'trusted' ,
} ,
}
} )
}
2026-06-22 14:45:04 -04:00
// Serve each real registry UI's WHOLE directory at its /app/<id>/ path, so the
// shell's bundled assets (qrcode.js, css, bg images, icons) resolve via their
// relative paths. The data endpoints each shell polls are mocked separately.
const DOCKER _UI = path . join ( _ _dirname , '..' , 'docker' )
for ( const [ prefixes , dir ] of [
[ [ '/app/bitcoin-core' , '/app/bitcoin-knots' , '/app/bitcoin-ui' ] , 'bitcoin-ui' ] ,
[ [ '/app/electrumx' , '/app/electrs' , '/app/archy-electrs-ui' ] , 'electrs-ui' ] ,
2026-07-15 09:25:38 +01:00
// lnd deliberately NOT here: the real lnd-ui shell reads poorly in the demo
// iframe, so /app/lnd/ gets a DEMO_APP_PAGES placeholder dashboard instead.
2026-06-22 14:45:04 -04:00
[ [ '/app/fedimint' , '/app/fedimintd' ] , 'fedimint-ui' ] ,
] ) {
for ( const p of prefixes ) app . use ( p , express . static ( path . join ( DOCKER _UI , dir ) ) )
}
// Shells that reference /app/<id>/assets/... resolve to the frontend's shared assets.
2026-07-14 10:23:08 -04:00
// Demo placeholder for mempool: the real explorer needs a synced chain +
// electrs, which the public demo doesn't run. Show a branded "not available
// in demo" page instead of a 502. Self-contained (inline styles/logo) so it
// renders identically from the mock origin in dev and in the demo stack.
app . get ( /^\/app\/mempool(\/.*)?$/ , ( _req , res ) => {
res . type ( 'html' ) . send ( ` <!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Mempool</title></head>
< body style = "margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#11131f;font-family:-apple-system,'Segoe UI',Roboto,sans-serif;" >
< div style = "text-align:center;padding:2rem;" >
< div style = "display:flex;gap:6px;justify-content:center;margin-bottom:1.25rem;" >
< div style = "width:34px;height:34px;border-radius:7px;background:#9339f4;" > < / d i v >
< div style = "width:34px;height:34px;border-radius:7px;background:#105fb0;" > < / d i v >
< div style = "width:34px;height:34px;border-radius:7px;background:#00a0dc;" > < / d i v >
< div style = "width:34px;height:34px;border-radius:7px;background:#00c896;" > < / d i v >
< / d i v >
< div style = "font-size:1.6rem;font-weight:700;color:#fff;letter-spacing:.02em;" > mempool < / d i v >
< p style = "color:rgba(255,255,255,.55);font-size:.95rem;margin-top:.75rem;" > Not available in the demo < / p >
< p style = "color:rgba(255,255,255,.35);font-size:.8rem;max-width:340px;margin:0.5rem auto 0;" >
The block explorer runs against your node ' s own synced chain — install Archipelago to explore mempool . space - style data privately .
< / p >
< / d i v >
< / b o d y > < / h t m l > ` )
} )
2026-06-22 14:45:04 -04:00
app . get ( /^\/app\/[^/]+\/assets\/(.*)$/ , ( req , res ) => res . redirect ( 302 , '/assets/' + req . params [ 0 ] ) )
2026-06-22 13:55:50 -04:00
// Dummy status for both the electrs-ui shell and the in-app ElectrumX sync screen.
app . get ( '/electrs-status' , ( _req , res ) => {
res . json ( {
status : 'ready' ,
synced : true ,
stale : false ,
error : null ,
bitcoin _height : 902418 ,
network _height : 902418 ,
indexed _height : 902418 ,
progress _pct : 100 ,
index _size : '58.2 GB' ,
tor _onion : 'electrumx' + randomHex ( 20 ) + '.onion' ,
} )
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
} )
2026-06-22 14:45:04 -04:00
// ── Bitcoin Core + Knots: per-implementation status (shell served statically) ─
2026-06-22 14:19:38 -04:00
function bitcoinStatusPayload ( impl ) {
const net = mockBitcoinNetworkInfo ( )
net . subversion = impl === 'knots' ? '/Satoshi:28.1.0/Knots:20250305/' : '/Satoshi:28.4.0/'
return {
ok : true , stale : false , updated _at _ms : Date . now ( ) , error : null ,
blockchain _info : mockBitcoinBlockchainInfo ( ) ,
network _info : net ,
index _info : {
txindex : { synced : true , best _block _height : 902418 } ,
blockfilterindex : { synced : true , best _block _height : 902418 } ,
} ,
zmq _notifications : [
{ type : 'pubhashblock' , address : 'tcp://127.0.0.1:28332' , hwm : 1000 } ,
{ type : 'pubrawtx' , address : 'tcp://127.0.0.1:28333' , hwm : 1000 } ,
] ,
}
}
app . get ( '/app/bitcoin-core/bitcoin-status' , ( _req , res ) => res . json ( bitcoinStatusPayload ( 'core' ) ) )
app . get ( '/app/bitcoin-knots/bitcoin-status' , ( _req , res ) => res . json ( bitcoinStatusPayload ( 'knots' ) ) )
2026-06-22 13:55:50 -04:00
2026-06-22 14:45:04 -04:00
// ── LND: the endpoints the lnd-ui shell polls (shell served statically) ──────
2026-06-22 14:19:38 -04:00
function lndGetinfo ( ) {
return {
alias : 'archipelago-lnd' , identity _pubkey : '02' + randomHex ( 32 ) ,
num _active _channels : 4 , num _inactive _channels : 0 , num _pending _channels : 0 , num _peers : 11 ,
block _height : 902418 , block _hash : randomHex ( 32 ) ,
synced _to _chain : true , synced _to _graph : true , version : '0.18.3-beta' ,
chains : [ { chain : 'bitcoin' , network : 'signet' } ] ,
}
}
function lndChannels ( ) {
const peers = [ [ 'ACINQ' , 5_000_000 , 2_450_000 ] , [ 'Wallet of Satoshi' , 2_000_000 , 1_200_000 ] ,
[ 'Voltage' , 10_000_000 , 4_500_000 ] , [ 'Kraken' , 3_000_000 , 1_800_000 ] ]
return {
channels : peers . map ( ( [ alias , cap , local ] ) => ( {
active : true , remote _pubkey : '02' + randomHex ( 32 ) , peer _alias : alias ,
capacity : String ( cap ) , local _balance : String ( local ) , remote _balance : String ( cap - local ) ,
total _satoshis _sent : '12840' , total _satoshis _received : '9120' , private : false ,
} ) ) ,
}
}
app . get ( '/proxy/lnd/v1/getinfo' , ( _req , res ) => res . json ( lndGetinfo ( ) ) )
app . get ( '/proxy/lnd/v1/channels' , ( _req , res ) => res . json ( lndChannels ( ) ) )
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
// 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.
2026-06-22 14:19:38 -04:00
app . get ( '/lnd-connect-info' , ( _req , res ) => res . json ( {
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
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 ,
2026-06-22 14:19:38 -04:00
grpcReachable : true , restReachable : true ,
tor _onion : 'lnd' + randomHex ( 16 ) + '.onion' , error : null ,
} ) )
2026-06-22 14:45:04 -04:00
// App catalog (the discover page tries this first, then /catalog.json).
app . get ( '/api/app-catalog' , async ( _req , res ) => {
try {
const json = await fs . readFile ( path . join ( _ _dirname , 'public' , 'catalog.json' ) , 'utf8' )
res . type ( 'application/json' ) . send ( json )
} catch { res . status ( 404 ) . json ( { error : 'not found' } ) }
} )
2026-06-22 14:19:38 -04:00
app . get ( '/api/container/logs' , ( _req , res ) => res . json ( {
logs : [
'[INF] LND: Version 0.18.3-beta commit=v0.18.3-beta' ,
'[INF] CHDB: Inserting 4 channel(s) into database' ,
'[INF] LND: Channel(s) active; synced_to_chain=true' ,
'[INF] PEER: Connected to 11 peers' ,
'[INF] RPCS: gRPC proxy started at 0.0.0.0:8080' ,
] . join ( '\n' ) ,
} ) )
2026-06-22 14:45:04 -04:00
app . get ( '/app/bitcoin-ui/bitcoin-status' , ( _req , res ) => res . json ( bitcoinStatusPayload ( 'knots' ) ) )
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
2026-06-22 14:45:04 -04:00
app . post ( [ '/app/bitcoin-ui/bitcoin-rpc/' , '/app/bitcoin-core/bitcoin-rpc/' , '/app/bitcoin-knots/bitcoin-rpc/' ] , ( req , res ) => {
2026-06-11 00:24:40 -04:00
const { id = 'bitcoin-ui' , method } = req . body || { }
const results = {
getblockchaininfo : mockBitcoinBlockchainInfo ( ) ,
getnetworkinfo : mockBitcoinNetworkInfo ( ) ,
getindexinfo : {
txindex : { synced : true , best _block _height : 902418 } ,
blockfilterindex : { synced : true , best _block _height : 902418 } ,
} ,
getzmqnotifications : [
{ type : 'pubhashblock' , address : 'tcp://127.0.0.1:28332' , hwm : 1000 } ,
{ type : 'pubrawtx' , address : 'tcp://127.0.0.1:28333' , hwm : 1000 } ,
] ,
getmempoolinfo : { loaded : true , size : 18452 , bytes : 62400000 , usage : 143000000 , maxmempool : 300000000 } ,
getpeerinfo : Array . from ( { length : 18 } , ( _ , index ) => ( { id : index , addr : ` 198.51.100. ${ index + 10 } :8333 ` , inbound : index % 3 === 0 } ) ) ,
}
if ( ! Object . prototype . hasOwnProperty . call ( results , method ) ) {
return res . json ( { id , result : null , error : { code : - 32601 , message : ` Method not found: ${ method } ` } } )
}
res . json ( { id , result : results [ method ] , error : null } )
} )
2026-06-22 14:45:04 -04:00
app . post ( [ '/app/bitcoin-ui/rpc/v1' , '/app/bitcoin-core/rpc/v1' , '/app/bitcoin-knots/rpc/v1' ] , ( req , res ) => {
2026-06-11 00:24:40 -04:00
const { method , params = { } } = req . body || { }
const now = new Date ( ) . toISOString ( )
switch ( method ) {
case 'bitcoin.relay-status' :
return res . json ( { result : bitcoinRelayStatusPayload ( ) , error : null } )
case 'bitcoin.relay-update-settings' :
bitcoinRelayMockState . settings = {
... bitcoinRelayMockState . settings ,
... params ,
}
return res . json ( { result : bitcoinRelayStatusPayload ( ) , error : null } )
case 'bitcoin.relay-request-peer' : {
const peer = bitcoinRelayMockState . trusted _nodes . find ( p => p . pubkey === params . peer _pubkey )
if ( ! peer ) return res . status ( 400 ) . json ( { error : { message : 'Peer is not in trusted nodes' } } )
const request = {
id : ` relay-demo- ${ Date . now ( ) } ` ,
direction : 'outbound' ,
status : 'pending' ,
peer _pubkey : peer . pubkey ,
peer _onion : peer . onion ,
peer _name : peer . name ,
message : params . message || '' ,
created _at : now ,
updated _at : now ,
}
bitcoinRelayMockState . requests . push ( request )
return res . json ( { result : { ok : true , request _id : request . id } , error : null } )
}
case 'bitcoin.relay-approve-request' :
case 'bitcoin.relay-reject-request' : {
const requestId = params . id || params . request _id
const request = bitcoinRelayMockState . requests . find ( r => r . id === requestId )
if ( ! request ) return res . status ( 404 ) . json ( { error : { message : ` Request not found: ${ requestId } ` } } )
request . status = method . endsWith ( 'approve-request' ) ? 'approved' : 'rejected'
request . updated _at = now
if ( request . status === 'approved' ) {
request . approved _endpoint = bitcoinRelayMockState . settings . https _endpoint || bitcoinRelayMockState . settings . tor _endpoint || bitcoinRelayMockState . settings . http _endpoint
request . credential _secret _path = '/var/lib/archipelago/secrets/bitcoin-relay-peer-demo.env'
}
return res . json ( { result : { ok : true , request _id : request . id } , error : null } )
}
case 'bitcoin.relay-create-tor-service' :
bitcoinRelayMockState . settings . allow _tor = true
bitcoinRelayMockState . settings . tor _endpoint = 'http://btc-relay-demoabcdefghijklmnop.onion/'
return res . json ( {
result : {
created : true ,
name : 'bitcoin-rpc' ,
onion _address : 'btc-relay-demoabcdefghijklmnop.onion' ,
} ,
error : null ,
} )
default :
return res . status ( 404 ) . json ( { error : { message : ` Unknown mock Bitcoin UI RPC method: ${ method } ` } } )
}
} )
2026-01-24 22:59:20 +00:00
// RPC endpoint
app . post ( '/rpc/v1' , ( req , res ) => {
const { method , params } = req . body
console . log ( ` [RPC] ${ method } ` )
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:56:37 +00:00
// Boot mode: return 502 during simulated startup delay
if ( DEV _MODE === 'boot' ) {
// Reset boot timer when browser does a fresh page load (server.echo with 'boot' message)
if ( method === 'server.echo' && params ? . message === 'boot-reset' ) {
BOOT _START _TIME = Date . now ( )
console . log ( ` [Boot] Timer RESET — simulating ${ BOOT _DELAY _MS / 1000 } s startup ` )
return res . status ( 502 ) . json ( { error : 'Server starting up (reset)' } )
}
const elapsed = Date . now ( ) - BOOT _START _TIME
if ( elapsed < BOOT _DELAY _MS ) {
const secs = Math . round ( elapsed / 1000 )
const total = Math . round ( BOOT _DELAY _MS / 1000 )
console . log ( ` [Boot] Server starting... ${ secs } s / ${ total } s ` )
return res . status ( 502 ) . json ( { error : 'Server starting up' } )
}
if ( elapsed < BOOT _DELAY _MS + 2000 ) {
console . log ( ` [Boot] Server is now READY (took ${ Math . round ( elapsed / 1000 ) } s) ` )
}
}
2026-01-24 22:59:20 +00:00
try {
switch ( method ) {
// Authentication endpoints
case 'auth.setup' : {
const { password } = params
if ( ! password || password . length < 8 ) {
return res . json ( {
error : {
code : - 32602 ,
message : 'Password must be at least 8 characters' ,
} ,
} )
}
// Set up user
userState . setupComplete = true
userState . passwordHash = password // In real app, bcrypt hash
console . log ( ` [Auth] User setup completed ` )
return res . json ( { result : { success : true } } )
}
case 'auth.isSetup' : {
return res . json ( { result : userState . setupComplete } )
}
case 'auth.onboardingComplete' : {
userState . onboardingComplete = true
console . log ( ` [Auth] Onboarding completed ` )
2026-02-17 15:03:34 +00:00
return res . json ( { result : true } )
2026-01-24 22:59:20 +00:00
}
case 'auth.isOnboardingComplete' : {
return res . json ( { result : userState . onboardingComplete } )
}
2026-03-02 08:34:13 +00:00
case 'auth.resetOnboarding' : {
userState . onboardingComplete = false
console . log ( '[Auth] Onboarding reset' )
return res . json ( { result : true } )
}
2026-06-22 09:28:05 -04:00
case 'app.filebrowser-token' : {
// The Cloud/Files UI exchanges this for a filebrowser auth cookie.
// The mock filebrowser endpoints don't validate it, so any token works.
return res . json ( { result : { token : ` demo-fb- ${ Date . now ( ) . toString ( 36 ) } ` } } )
}
2026-02-17 15:03:34 +00:00
case 'node.did' : {
const mockDid = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'
const mockPubkey = 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456'
return res . json ( { result : { did : mockDid , pubkey : mockPubkey } } )
}
case 'node.nostr-publish' : {
return res . json ( { result : { event _id : 'mock-event-id' , success : 2 , failed : 0 } } )
}
case 'node.nostr-pubkey' : {
2026-07-22 02:38:52 -04:00
// Exact shape of the real handler (node.rs handle_node_nostr_pubkey):
// the node's dedicated discovery key — hex + genuine bech32 npub
// (deliberately NOT one of the personal identity keys; discovery
// presence events are always signed with this separate node key).
return res . json ( { result : {
nostr _pubkey : 'c9f0a2e84b71d3568e0f4a6b2c8d1e97a3b5c7d9e1f2a4b6c8d0e2f4a6b8c0d2' ,
nostr _npub : 'npub1e8c296ztw8f4drs0ff4jerg7j73mt37eu8e2fdkg6r30ff4ccrfq8j5vuy' ,
} } )
2026-02-17 15:03:34 +00:00
}
2026-03-02 08:34:13 +00:00
case 'node.signChallenge' : {
const { challenge } = params || { }
const mockSig = Buffer . from ( ` mock-sig- ${ challenge || 'challenge' } ` ) . toString ( 'hex' )
return res . json ( { result : { signature : mockSig } } )
}
2026-03-22 03:30:21 +00:00
case 'identity.verify' : {
return res . json ( { result : { valid : true } } )
}
2026-03-31 01:41:24 +01:00
// BIP-39 seed management (mock for dev mode)
case 'seed.generate' : {
const mockWords = [
'abandon' , 'ability' , 'able' , 'about' , 'above' , 'absent' ,
'absorb' , 'abstract' , 'absurd' , 'abuse' , 'access' , 'accident' ,
'account' , 'accuse' , 'achieve' , 'acid' , 'acoustic' , 'acquire' ,
'across' , 'act' , 'action' , 'actor' , 'actress' , 'actual'
]
return res . json ( { result : { words : mockWords } } )
}
case 'seed.verify' : {
const mockDid = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'
return res . json ( { result : { verified : true , did : mockDid , nostr _npub : 'npub1mock...' } } )
}
case 'seed.restore' : {
const mockDid = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'
return res . json ( { result : { did : mockDid , nostr _npub : 'npub1mock...' , restored : true } } )
}
case 'seed.save-encrypted' : {
return res . json ( { result : { saved : true } } )
}
case 'seed.status' : {
return res . json ( { result : { has _seed : true , is _legacy : false , identity _count : 1 , next _index : 1 } } )
}
2026-03-02 08:34:13 +00:00
case 'node.createBackup' : {
const { passphrase } = params || { }
if ( ! passphrase ) {
return res . json ( { error : { code : - 32602 , message : 'Missing passphrase' } } )
}
const mockDid = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'
const mockPubkey = 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456'
return res . json ( {
result : {
version : 1 ,
did : mockDid ,
pubkey : mockPubkey ,
kid : ` ${ mockDid } #key-1 ` ,
encrypted : true ,
blob : Buffer . from ( ` mock-encrypted-backup- ${ passphrase } ` ) . toString ( 'base64' ) ,
timestamp : new Date ( ) . toISOString ( ) ,
} ,
} )
}
2026-01-24 22:59:20 +00:00
case 'auth.login' : {
const { password } = params
if ( ! userState . setupComplete ) {
return res . json ( {
error : {
code : - 32603 ,
message : 'User not set up. Please complete setup first.' ,
} ,
} )
}
// Simple password check (in real app, use bcrypt)
if ( password !== userState . passwordHash && password !== MOCK _PASSWORD ) {
return res . json ( {
error : {
code : - 32603 ,
message : 'Password Incorrect' ,
} ,
} )
}
const sessionId = ` session- ${ Date . now ( ) } `
sessions . set ( sessionId , {
createdAt : new Date ( ) ,
} )
res . cookie ( 'session' , sessionId , {
httpOnly : true ,
maxAge : 24 * 60 * 60 * 1000 ,
} )
return res . json ( { result : null } )
}
case 'auth.logout' : {
const sessionId = req . cookies ? . session
if ( sessionId ) {
sessions . delete ( sessionId )
}
res . clearCookie ( 'session' )
return res . json ( { result : null } )
}
2026-03-13 23:40:29 +00:00
case 'server.set-name' : {
const name = ( params ? . name || '' ) . trim ( )
if ( ! name || name . length > 64 ) {
return res . json ( { error : { code : - 1 , message : 'Name must be 1-64 characters' } } )
}
mockData [ 'server-info' ] . name = name
broadcastUpdate ( )
return res . json ( { result : { name } } )
}
2026-01-24 22:59:20 +00:00
case 'server.echo' : {
return res . json ( { result : { message : params ? . message || 'Hello from Archipelago!' } } )
}
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
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 } } )
}
2026-01-24 22:59:20 +00:00
case 'server.time' : {
return res . json ( {
result : {
now : new Date ( ) . toISOString ( ) ,
uptime : process . uptime ( ) ,
} ,
} )
}
case 'server.metrics' : {
2026-03-09 13:03:53 +00:00
// Slightly randomize so the dashboard feels alive
2026-01-24 22:59:20 +00:00
return res . json ( {
result : {
2026-03-09 13:03:53 +00:00
cpu : + ( 12 + Math . random ( ) * 18 ) . toFixed ( 1 ) ,
memory : + ( 58 + Math . random ( ) * 8 ) . toFixed ( 1 ) ,
disk : + ( 34 + Math . random ( ) * 3 ) . toFixed ( 1 ) ,
2026-01-24 22:59:20 +00:00
} ,
} )
}
case 'marketplace.get' : {
const mockApps = [
{
id : 'bitcoin' ,
title : 'Bitcoin Core' ,
2026-03-09 13:03:53 +00:00
description : 'Full Bitcoin node — validate transactions, enforce consensus rules, and support the network. The foundation of sovereignty.' ,
version : '27.1' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/bitcoin-core.png' ,
2026-03-09 13:03:53 +00:00
author : 'Bitcoin Core' ,
2026-01-24 22:59:20 +00:00
license : 'MIT' ,
2026-03-09 13:03:53 +00:00
category : 'Bitcoin' ,
} ,
{
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' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/electrumx.webp' ,
2026-03-09 13:03:53 +00:00
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' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/mempool.webp' ,
2026-03-09 13:03:53 +00:00
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' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/btcpay-server.png' ,
2026-03-09 13:03:53 +00:00
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.' ,
2026-03-18 19:24:52 +00:00
version : '0.10.0' ,
2026-03-09 13:03:53 +00:00
icon : '/assets/img/app-icons/fedimint.png' ,
author : 'Fedimint' ,
license : 'MIT' ,
category : 'Bitcoin' ,
} ,
2026-03-18 19:24:52 +00:00
{
id : 'thunderhub' ,
title : 'ThunderHub' ,
description : 'Lightning node management UI — manage channels, send and receive payments, view routing fees, and monitor your node health.' ,
version : '0.13.31' ,
icon : '/assets/img/app-icons/thunderhub.svg' ,
author : 'Anthony Potdevin' ,
license : 'MIT' ,
category : 'Bitcoin' ,
} ,
2026-03-09 13:03:53 +00:00
{
id : 'vaultwarden' ,
title : 'Vaultwarden' ,
description : 'Self-hosted password manager compatible with Bitwarden clients. Keep your credentials under your own roof.' ,
version : '1.32.5' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/vaultwarden.webp' ,
2026-03-09 13:03:53 +00:00
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' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/nextcloud.webp' ,
2026-03-09 13:03:53 +00:00
author : 'Nextcloud GmbH' ,
license : 'AGPL-3.0' ,
category : 'Cloud' ,
2026-01-24 22:59:20 +00:00
} ,
{
2026-03-09 13:03:53 +00:00
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' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/nostrudel.svg' ,
2026-03-09 13:03:53 +00:00
author : 'nostr-rs-relay' ,
2026-01-24 22:59:20 +00:00
license : 'MIT' ,
2026-03-09 13:03:53 +00:00
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' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/homeassistant.png' ,
2026-03-09 13:03:53 +00:00
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' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/archipelago-a.svg' ,
2026-03-09 13:03:53 +00:00
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' ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
icon : '/assets/img/app-icons/tor.svg' ,
2026-03-09 13:03:53 +00:00
author : 'Tor Project' ,
license : 'BSD-3' ,
category : 'Privacy' ,
2026-01-24 22:59:20 +00:00
} ,
]
2026-03-09 13:03:53 +00:00
2026-01-24 22:59:20 +00:00
return res . json ( { result : mockApps } )
}
case 'server.update' :
case 'server.restart' :
case 'server.shutdown' : {
return res . json ( { result : 'ok' } )
}
case 'package.install' : {
2026-03-09 17:09:59 +00:00
const { id , url , dockerImage , version } = params
installPackage ( id , url , { dockerImage , version } ) . catch ( err => {
2026-01-24 22:59:20 +00:00
console . error ( ` [RPC] Installation failed: ` , err . message )
} )
2026-03-09 17:09:59 +00:00
2026-01-24 22:59:20 +00:00
return res . json ( { result : ` job- ${ Date . now ( ) } ` } )
}
case 'package.uninstall' : {
const { id } = params
uninstallPackage ( id ) . catch ( err => {
console . error ( ` [RPC] Uninstall failed: ` , err . message )
} )
return res . json ( { result : 'ok' } )
}
case 'package.start' :
case 'package.stop' :
case 'package.restart' : {
return res . json ( { result : 'ok' } )
}
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
case 'auth.totp.status' : {
return res . json ( { result : { enabled : false } } )
}
case 'auth.totp.setup.begin' : {
return res . json ( {
result : {
qr _svg : '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><rect width="200" height="200" fill="#fff"/><text x="100" y="100" text-anchor="middle" font-size="12" fill="#333">Mock QR Code</text></svg>' ,
secret _base32 : 'JBSWY3DPEHPK3PXP' ,
pending _token : 'mock-pending-token' ,
} ,
} )
}
case 'auth.totp.setup.confirm' : {
return res . json ( {
result : {
enabled : true ,
backup _codes : [ 'ABCD-EFGH' , 'JKLM-NPQR' , 'STUV-WXYZ' , '2345-6789' , 'ABCD-2345' , 'EFGH-6789' , 'JKLM-STUV' , 'NPQR-WXYZ' ] ,
} ,
} )
}
case 'auth.totp.disable' : {
return res . json ( { result : { disabled : true } } )
}
case 'auth.login.totp' :
case 'auth.login.backup' : {
return res . json ( { result : { success : true } } )
}
2026-03-09 13:03:53 +00:00
// =========================================================================
// 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 } } )
}
2026-03-22 03:30:21 +00:00
case 'identity.list' : {
return res . json ( {
result : {
identities : [
{
id : 'id-primary' ,
name : 'Primary' ,
purpose : 'Main node identity' ,
pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' ,
created _at : '2026-01-10T08:00:00Z' ,
is _default : true ,
nostr _pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
nostr _npub : 'npub1mockprimaryidentitypubkeyvalue0000000000000000000000000abc' ,
profile : {
display _name : 'Archipelago Node' ,
about : 'Self-sovereign Bitcoin node' ,
nip05 : 'satoshi@archipelago.local' ,
lud16 : 'satoshi@getalby.com' ,
2026-07-17 02:12:54 +01:00
picture : '/demo-avatars/satoshi.svg' ,
2026-03-22 03:30:21 +00:00
} ,
} ,
{
id : 'id-anon' ,
name : 'Anonymous' ,
purpose : 'Privacy-focused browsing' ,
pubkey : 'f6e5d4c3b2a19876543210fedcba9876543210fedcba9876543210fedcba98' ,
did : 'did:key:z6MkvWkza1fMBWhKnYE3CgMgxHLKbN8NmbFRqHcECF4oGrwx' ,
created _at : '2026-02-14T12:30:00Z' ,
is _default : false ,
nostr _pubkey : 'f6e5d4c3b2a19876543210fedcba9876543210fedcba9876543210fedcba98' ,
nostr _npub : 'npub1mockanonidentitypubkeyvalue000000000000000000000000000xyz' ,
2026-07-17 02:12:54 +01:00
profile : { display _name : 'Anon' , picture : '/demo-avatars/anon.svg' } ,
2026-03-22 03:30:21 +00:00
} ,
{
id : 'id-merchant' ,
name : 'Merchant' ,
purpose : 'BTCPay & commerce' ,
pubkey : '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' ,
did : 'did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WNhSwaxN21V' ,
created _at : '2026-03-01T16:00:00Z' ,
is _default : false ,
nostr _pubkey : '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' ,
nostr _npub : 'npub1mockmerchantidentitypubkeyvalue000000000000000000000000def' ,
2026-07-17 02:12:54 +01:00
profile : { display _name : 'My Shop' , website : 'https://myshop.onion' , picture : '/demo-avatars/business.svg' } ,
2026-03-22 03:30:21 +00:00
} ,
] ,
} ,
} )
}
case 'identity.get' : {
const id = params ? . id || 'id-primary'
return res . json ( {
result : {
id ,
name : 'Primary' ,
purpose : 'Main node identity' ,
pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' ,
created _at : '2026-01-10T08:00:00Z' ,
is _default : true ,
nostr _pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
nostr _npub : 'npub1mockprimaryidentitypubkeyvalue0000000000000000000000000abc' ,
profile : {
display _name : 'Archipelago Node' ,
about : 'Self-sovereign Bitcoin node' ,
2026-07-17 02:12:54 +01:00
picture : '/demo-avatars/satoshi.svg' ,
2026-03-22 03:30:21 +00:00
} ,
} ,
} )
}
case 'identity.export-keys' : {
return res . json ( {
result : {
ed25519 _secret _hex : 'deadbeef' . repeat ( 8 ) ,
nostr _secret _hex : 'cafebabe' . repeat ( 8 ) ,
nostr _nsec : 'nsec1mockexportedkeyvalue00000000000000000000000000000000000abc' ,
} ,
} )
}
case 'identity.update-profile' : {
return res . json ( { result : { success : true } } )
}
case 'identity.publish-profile' : {
return res . json ( { result : { event _id : ` evt- ${ Date . now ( ) . toString ( 36 ) } ` } } )
}
case 'identity.list' : {
return res . json ( {
result : {
identities : [
{
id : 'id-primary' ,
name : 'Primary' ,
purpose : 'personal' ,
pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' ,
created _at : '2026-01-10T08:00:00Z' ,
is _default : true ,
nostr _pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
nostr _npub : 'npub1598eg0y7m08htfzjfmzv6zjvf5u7p00dr9w0yfamaxqhkwlryckq5dh9ee' ,
profile : {
display _name : 'Satoshi' ,
about : 'Running a sovereign Bitcoin node' ,
nip05 : 'satoshi@archipelago.local' ,
lud16 : 'satoshi@getalby.com' ,
2026-07-17 02:12:54 +01:00
picture : '/demo-avatars/satoshi.svg' ,
2026-03-22 03:30:21 +00:00
} ,
} ,
{
id : 'id-business' ,
name : 'Business' ,
purpose : 'business' ,
pubkey : 'f6e5d4c3b2a10987654321fedcba0987654321fedcba0987654321fedcba0987' ,
did : 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9' ,
created _at : '2026-02-05T12:30:00Z' ,
is _default : false ,
nostr _pubkey : 'f6e5d4c3b2a10987654321fedcba0987654321fedcba0987654321fedcba0987' ,
nostr _npub : 'npub17m9mx9kk2ry8p3xfkq0070fjlph9r9l0dj0y8fn0zrlj80ekjph7jxmxfg' ,
profile : {
display _name : 'Archy Consulting' ,
about : 'Bitcoin infrastructure services' ,
2026-07-17 02:12:54 +01:00
picture : '/demo-avatars/business.svg' ,
2026-03-22 03:30:21 +00:00
} ,
} ,
{
id : 'id-anon' ,
name : 'Anonymous' ,
purpose : 'anonymous' ,
pubkey : 'deadbeef00112233445566778899aabbccddeeff00112233445566778899aabb' ,
did : 'did:key:z6MknGc3ocHs3zdPiJbnaaqDi5hjrZo4HzmQnwzaxWhAbWAs' ,
created _at : '2026-03-01T18:00:00Z' ,
is _default : false ,
2026-07-17 02:12:54 +01:00
profile : { picture : '/demo-avatars/anon.svg' } ,
2026-03-22 03:30:21 +00:00
} ,
] ,
} ,
} )
}
case 'identity.get' : {
const id = params ? . id || 'id-primary'
const identities = {
'id-primary' : {
id : 'id-primary' , name : 'Primary' , purpose : 'personal' ,
pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' ,
nostr _pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
nostr _npub : 'npub1598eg0y7m08htfzjfmzv6zjvf5u7p00dr9w0yfamaxqhkwlryckq5dh9ee' ,
is _default : true , created _at : '2026-01-10T08:00:00Z' ,
2026-07-17 02:12:54 +01:00
profile : { display _name : 'Satoshi' , about : 'Running a sovereign Bitcoin node' , picture : '/demo-avatars/satoshi.svg' } ,
2026-03-22 03:30:21 +00:00
} ,
}
return res . json ( { result : identities [ id ] || identities [ 'id-primary' ] } )
}
case 'identity.export-keys' : {
const { password } = params || { }
if ( password !== MOCK _PASSWORD ) {
return res . json ( { error : { code : - 32603 , message : 'Incorrect password' } } )
}
return res . json ( {
result : {
ed25519 _secret _hex : 'mock_ed25519_secret_' + '0' . repeat ( 40 ) ,
nostr _secret _hex : 'mock_nostr_secret_' + '0' . repeat ( 44 ) ,
nostr _nsec : 'nsec1mockkeymockkeymockkeymockkeymockkeymockkeymockkeymockkeymo' ,
} ,
} )
}
case 'identity.update-profile' : {
console . log ( ` [Identity] Updated profile for: ${ params ? . id } ` )
return res . json ( { result : { success : true } } )
}
case 'identity.publish-profile' : {
console . log ( ` [Identity] Published profile for: ${ params ? . id } ` )
return res . json ( { result : { event _id : ` nostr-event- ${ Date . now ( ) . toString ( 36 ) } ` } } )
}
2026-03-09 13:03:53 +00:00
// =========================================================================
// Nostr
// =========================================================================
feat(demo): network/wallet dummy data — profits, federation, VPN, nostr, visibility
- wallet.networking-profits = 5,231,978 sats (content 3,180,000 / routing
1,281,978 / relay 770,000); 6 labelled profit transactions added to the wallet
history (1-2 per type: content sale, routing fee, file/mesh relay) — labels are
production-ready.
- federation.list (the Web5 Federation container's method) now returns the 12
demo nodes (was unhandled → empty).
- vpn.status: connected WireGuard with peers + traffic.
- nostr.list-relays / nostr.get-stats: 5 relays (3 connected).
- network.get/set-visibility: interactive, persisted per demo session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:18:29 -04:00
case 'nostr.list-relays' : {
return res . json ( { result : { relays : [
{ url : 'wss://relay.damus.io' , connected : true , enabled : true , added _at : '2026-04-02T10:00:00Z' } ,
{ url : 'wss://nos.lol' , connected : true , enabled : true , added _at : '2026-04-02T10:00:00Z' } ,
{ url : 'wss://relay.snort.social' , connected : true , enabled : true , added _at : '2026-04-10T08:30:00Z' } ,
{ url : 'wss://nostr.wine' , connected : false , enabled : true , added _at : '2026-05-01T14:00:00Z' } ,
{ url : 'wss://relay.archipelago.lan' , connected : true , enabled : false , added _at : '2026-05-20T09:15:00Z' } ,
] } } )
}
case 'nostr.get-stats' : {
return res . json ( { result : { total _relays : 5 , connected _count : 3 , enabled _count : 4 } } )
}
2026-03-09 13:03:53 +00:00
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 } } )
}
feat(demo): network/wallet dummy data — profits, federation, VPN, nostr, visibility
- wallet.networking-profits = 5,231,978 sats (content 3,180,000 / routing
1,281,978 / relay 770,000); 6 labelled profit transactions added to the wallet
history (1-2 per type: content sale, routing fee, file/mesh relay) — labels are
production-ready.
- federation.list (the Web5 Federation container's method) now returns the 12
demo nodes (was unhandled → empty).
- vpn.status: connected WireGuard with peers + traffic.
- nostr.list-relays / nostr.get-stats: 5 relays (3 connected).
- network.get/set-visibility: interactive, persisted per demo session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:18:29 -04:00
// VPN (WireGuard) status
case 'vpn.status' : {
return res . json ( { result : {
connected : true ,
provider : 'wireguard' ,
interface : 'wg0' ,
ip _address : '10.64.12.7' ,
hostname : 'archipelago-demo' ,
peers _connected : 3 ,
bytes _in : 1_843_201_004 ,
bytes _out : 642_889_310 ,
} } )
}
2026-07-17 02:32:02 +01:00
// Fake tunnel provisioning so the companion remote-access onboarding
// (CompanionIntroOverlay) walks the same steps as a real node. The keys
// are not real; importing this config yields a harmless dead tunnel.
case 'vpn.list-peers' : {
return res . json ( { result : { peers : mockState . vpnPeers || [ ] } } )
}
case 'vpn.create-peer' :
case 'vpn.peer-config' : {
const peerName = params ? . name || 'device'
if ( ! mockState . vpnPeers ) mockState . vpnPeers = [ ]
if ( ! mockState . vpnPeers . some ( ( p ) => p . name === peerName ) ) {
mockState . vpnPeers . push ( { name : peerName , ip : '10.44.0.7' } )
}
const config = [
'[Interface]' ,
'PrivateKey = 2DEMOdemoDEMOdemoDEMOdemoDEMOdemoDEMOdem0=' ,
'Address = 10.44.0.7/32' ,
'DNS = 1.1.1.1' ,
'' ,
'[Peer]' ,
'PublicKey = 3DEMOdemoDEMOdemoDEMOdemoDEMOdemoDEMOdem1=' ,
'Endpoint = demo.archipelago-foundation.org:51820' ,
'AllowedIPs = 10.44.0.0/16' ,
'PersistentKeepalive = 25' ,
] . join ( '\n' )
return res . json ( { result : { config , peer _ip : '10.44.0.7' } } )
}
feat(demo): network/wallet dummy data — profits, federation, VPN, nostr, visibility
- wallet.networking-profits = 5,231,978 sats (content 3,180,000 / routing
1,281,978 / relay 770,000); 6 labelled profit transactions added to the wallet
history (1-2 per type: content sale, routing fee, file/mesh relay) — labels are
production-ready.
- federation.list (the Web5 Federation container's method) now returns the 12
demo nodes (was unhandled → empty).
- vpn.status: connected WireGuard with peers + traffic.
- nostr.list-relays / nostr.get-stats: 5 relays (3 connected).
- network.get/set-visibility: interactive, persisted per demo session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:18:29 -04:00
// Node visibility — interactive (persisted per demo session)
case 'network.get-visibility' : {
2026-07-17 02:12:54 +01:00
// No onion_address in the demo: the public showcase shouldn't display
// a Tor address (even a fake one) on the Node Visibility card.
feat(demo): network/wallet dummy data — profits, federation, VPN, nostr, visibility
- wallet.networking-profits = 5,231,978 sats (content 3,180,000 / routing
1,281,978 / relay 770,000); 6 labelled profit transactions added to the wallet
history (1-2 per type: content sale, routing fee, file/mesh relay) — labels are
production-ready.
- federation.list (the Web5 Federation container's method) now returns the 12
demo nodes (was unhandled → empty).
- vpn.status: connected WireGuard with peers + traffic.
- nostr.list-relays / nostr.get-stats: 5 relays (3 connected).
- network.get/set-visibility: interactive, persisted per demo session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:18:29 -04:00
return res . json ( { result : {
visibility : mockState . nodeVisibility ,
} } )
}
case 'network.set-visibility' : {
const v = params ? . visibility
if ( [ 'hidden' , 'discoverable' , 'public' ] . includes ( v ) ) mockState . nodeVisibility = v
return res . json ( { result : {
visibility : mockState . nodeVisibility ,
} } )
}
2026-03-09 13:03:53 +00:00
// =========================================================================
// Content & Network
// =========================================================================
2026-03-22 03:30:21 +00:00
case 'content.browse-peer' : {
const onion = params ? . onion || ''
2026-07-15 10:35:42 +01:00
return res . json ( { result : { items : peerCatalogFor ( onion ) } } )
}
case 'content.preview-peer' : {
// Serve the item's committed preview image (film poster, book cover, or
// the photo itself) so the demo NEVER renders a broken thumbnail.
const entry = PEER _LIBRARY . find ( it => it . id === params ? . content _id )
const previewAbs = entry && ( entry . preview || ( entry . mime _type . startsWith ( 'image/' ) && entry . disk ) )
if ( previewAbs ) {
try {
const data = fsSync . readFileSync ( typeof previewAbs === 'string' ? previewAbs : entry . disk )
return res . json ( { result : { data : data . toString ( 'base64' ) , content _type : 'image/jpeg' } } )
} catch { /* fall through to no preview (icon fallback) */ }
}
return res . json ( { result : { } } )
}
case 'content.download-peer' : {
// Free items backed by a real committed file stream the actual bytes —
// music plays, photos open, documents read. Anything else gets the
// demo placeholder note.
const entry = PEER _LIBRARY . find ( it => it . id === params ? . content _id )
if ( entry ? . disk ) {
try {
const data = fsSync . readFileSync ( entry . disk )
return res . json ( { result : { data : data . toString ( 'base64' ) , content _type : entry . mime _type } } )
} catch { /* fall through to placeholder */ }
}
const placeholder = Buffer . from (
` Archipelago demo — " ${ entry ? . filename || params ? . content _id || 'file' } " \n \n ` +
'This is sample shared content. On a real node this would be the actual file.\n' ,
'utf-8'
) . toString ( 'base64' )
return res . json ( { result : { data : placeholder , content _type : 'text/plain' } } )
2026-03-22 03:30:21 +00:00
}
2026-03-09 13:03:53 +00:00
case 'content.remove' : {
return res . json ( { result : { success : true } } )
}
case 'content.set-pricing' : {
return res . json ( { result : { success : true } } )
}
2026-06-22 10:53:05 -04:00
// ── Demo paid-content / "buying files" flow ───────────────────────────
// Every payment path resolves to success so visitors can experience the
// full buy → unlock journey with testnet addresses and invoices.
case 'content.owned-list' : {
return res . json ( { result : { items : [ ] } } )
}
case 'content.owned-get' :
case 'content.download-peer-paid' :
case 'content.download-peer-invoice' :
case 'content.download-peer-onchain' : {
2026-07-15 09:53:53 +01:00
// Deduct the price from the chosen rail so demo balances react.
const paid = params ? . price _sats || 0
if ( paid > 0 && method === 'content.download-peer-paid' ) {
if ( params ? . method === 'ark' ) walletState . ark _sats = Math . max ( 0 , walletState . ark _sats - paid )
else if ( params ? . method === 'fedimint' ) {
const fed = ( mockState . federations || [ ] ) [ 0 ]
if ( fed ) fed . balance _sats = Math . max ( 0 , ( fed . balance _sats || 0 ) - paid )
} else walletState . ecash _sats = Math . max ( 0 , walletState . ecash _sats - paid )
}
2026-06-22 10:53:05 -04:00
const filename = params ? . filename || 'demo-content'
const body = Buffer . from (
` Archipelago demo — " ${ filename } " \n \n This is sample paid content delivered over the ` +
` node-to-node content market. On a real node this would be the actual file you purchased. \n ` ,
'utf-8'
) . toString ( 'base64' )
return res . json ( { result : { data : body , mime _type : 'text/plain' , ecash _backend : params ? . method || 'cashu' } } )
}
case 'content.request-invoice' : {
const price = params ? . price _sats || 500
2026-06-22 16:34:12 -04:00
const payment _hash = randomHex ( 32 )
invoiceRequestedAt . set ( payment _hash , Date . now ( ) )
2026-06-22 10:53:05 -04:00
return res . json ( {
result : {
bolt11 : 'lntb' + price + '1p' + randomHex ( 50 ) ,
2026-06-22 16:34:12 -04:00
payment _hash ,
2026-06-22 10:53:05 -04:00
price _sats : price ,
} ,
} )
}
case 'content.invoice-status' : {
2026-06-22 16:34:12 -04:00
// Demo: leave the QR on screen for ~2s before the invoice "settles".
const at = invoiceRequestedAt . get ( params ? . payment _hash )
const paid = ! at || ( Date . now ( ) - at ) >= 2000
return res . json ( { result : { paid } } )
}
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
// ── Networking profits: per-service pricing (interactive) ──────────────
case 'streaming.list-services' : {
return res . json ( { result : { services : mockState . streamingServices || [ ] } } )
}
2026-07-17 03:07:30 +01:00
case 'streaming.list-sessions' : {
const mkTime = ( minsAgo ) => new Date ( Date . now ( ) - minsAgo * 60_000 ) . toISOString ( )
return res . json ( { result : {
sessions : [
{ id : 'sess-demo-1' , peer _id : 'npub1walker…k3u9' , service _id : 'content-download' , metric : 'bytes' , allotment : 524_288_000 , used : 231_211_008 , paid _sats : 500 , created _at : mkTime ( 134 ) , last _topup _at : mkTime ( 22 ) , expires _at : '' , active : true } ,
{ id : 'sess-demo-2' , peer _id : 'npub1sailor…m2xq' , service _id : 'nostr-relay' , metric : 'milliseconds' , allotment : 10_800_000 , used : 4_920_000 , paid _sats : 30 , created _at : mkTime ( 82 ) , last _topup _at : mkTime ( 82 ) , expires _at : mkTime ( - 98 ) , active : true } ,
{ id : 'sess-demo-3' , peer _id : 'npub1nomad…t7rd' , service _id : 'content-download' , metric : 'bytes' , allotment : 104_857_600 , used : 88_080_384 , paid _sats : 100 , created _at : mkTime ( 9 ) , last _topup _at : mkTime ( 9 ) , expires _at : '' , active : true } ,
] ,
total _active : 3 ,
total _revenue _sats : 770_000 ,
revenue _by _service : {
'content-download' : 512_400 ,
'nostr-relay' : 201_600 ,
'api-access' : 56_000 ,
} ,
} } )
}
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
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 } } )
}
2026-06-22 16:34:12 -04:00
// ── Wallet settings: Cashu mints + Fedimint federations (interactive) ──
case 'streaming.list-mints' : {
return res . json ( { result : { mints : mockState . cashuMints } } )
}
case 'streaming.configure-mints' : {
if ( Array . isArray ( params ? . mints ) ) mockState . cashuMints = params . mints
return res . json ( { result : { success : true , mints : mockState . cashuMints } } )
}
case 'wallet.fedimint-list' : {
return res . json ( { result : { federations : mockState . federations } } )
}
case 'wallet.fedimint-join' : {
const fed = { federation _id : 'fed1' + randomHex ( 28 ) , name : 'Joined Federation' , balance _sats : 0 }
mockState . federations . push ( fed )
return res . json ( { result : { success : true , ... fed } } )
}
case 'wallet.fedimint-balance' : {
const total = ( mockState . federations || [ ] ) . reduce ( ( s , f ) => s + ( f . balance _sats || 0 ) , 0 )
return res . json ( { result : { balance _sats : total } } )
2026-06-22 10:53:05 -04:00
}
feat(ui): Ark wallet tab, balances, history badge + demo mocks
Ark tab in Wallet Settings (status, balances, receive address,
board/offboard, server config via wallet.ark-*), teal Ark badge in the
transactions modal, Ark row on the home wallet card (hidden until barkd
reports a balance), official bark icon, and wallet.ark-* mocks so the
public demo renders the tab.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 21:57:12 +01:00
case 'wallet.ark-status' : {
return res . json ( {
result : {
available : true ,
wallet _ready : true ,
wallet : { network : walletState . ark _config . network } ,
ark _info : { server : walletState . ark _config . ark _server } ,
config : walletState . ark _config ,
} ,
} )
}
case 'wallet.ark-balance' : {
return res . json ( {
result : {
balance _sats : walletState . ark _sats ,
spendable _sats : walletState . ark _sats ,
pending _sats : walletState . ark _pending _sats ,
onchain _sats : walletState . ark _onchain _sats ,
} ,
} )
}
case 'wallet.ark-address' : {
const onchain = params ? . onchain === true
const address = onchain ? 'tb1q' + randomHex ( 16 ) : 'tark1' + randomHex ( 28 )
return res . json ( { result : { address , onchain } } )
}
case 'wallet.ark-board' : {
const amt = params ? . amount _sats ? ? walletState . ark _onchain _sats
walletState . ark _onchain _sats = Math . max ( 0 , walletState . ark _onchain _sats - amt )
walletState . ark _sats += amt
return res . json ( { result : { boarded _sats : amt } } )
}
case 'wallet.ark-offboard' : {
const amt = walletState . ark _sats
walletState . ark _sats = 0
walletState . ark _onchain _sats += amt
return res . json ( { result : { offboarded _sats : amt } } )
}
case 'wallet.ark-send' : {
const amt = params ? . amount _sats || 1000
walletState . ark _sats = Math . max ( 0 , walletState . ark _sats - amt )
return res . json ( { result : { sent : true , movement : { id : 1 , effective _balance _sat : - amt } } } )
}
case 'wallet.ark-invoice' : {
return res . json ( { result : { invoice : 'lntbs' + randomHex ( 64 ) } } )
}
case 'wallet.ark-history' : {
return res . json ( { result : { transactions : [ ] } } )
}
case 'wallet.ark-configure' : {
const cfg = { ... walletState . ark _config }
for ( const key of [ 'network' , 'ark_server' , 'esplora' ] ) {
if ( typeof params ? . [ key ] === 'string' && params [ key ] . trim ( ) ) cfg [ key ] = params [ key ] . trim ( )
}
walletState . ark _config = cfg
return res . json ( { result : { config : cfg } } )
}
2026-06-22 10:53:05 -04:00
case 'content.request-onchain' : {
const price = params ? . price _sats || 500
return res . json ( {
result : {
address : 'tb1q' + randomHex ( 19 ) ,
amount _sats : price ,
content _id : params ? . content _id || '' ,
expires _at : new Date ( Date . now ( ) + 3600000 ) . toISOString ( ) ,
} ,
} )
}
case 'content.onchain-status' : {
return res . json ( { result : { paid : true , status : 'confirmed' , confirmations : 1 } } )
}
2026-06-22 10:55:13 -04:00
// ── FIPS encrypted-mesh transport (shows as fully active in the demo) ──
case 'fips.status' : {
return res . json ( {
result : {
installed : true ,
version : '0.4.2' ,
service _state : 'active' ,
upstream _service _state : 'active' ,
service _active : true ,
key _present : true ,
npub : 'npub1' + randomHex ( 28 ) ,
authenticated _peer _count : 5 ,
anchor _connected : true ,
} ,
} )
}
case 'fips.list-seed-anchors' : {
return res . json ( {
result : {
seed _anchors : [
{ npub : 'npub1' + randomHex ( 28 ) , address : 'fips.v0l.io:8443' , transport : 'tcp' , label : 'Public anchor' } ,
{ npub : 'npub1' + randomHex ( 28 ) , address : 'anchor.archipelago.lan:8443' , transport : 'tcp' , label : 'Federation anchor' } ,
] ,
} ,
} )
}
case 'fips.add-seed-anchor' :
case 'fips.remove-seed-anchor' : {
const seed = [
{ npub : 'npub1' + randomHex ( 28 ) , address : params ? . address || 'fips.v0l.io:8443' , transport : 'tcp' , label : params ? . label || 'Anchor' } ,
]
return res . json ( { result : { seed _anchors : seed , apply : [ { npub : seed [ 0 ] . npub , ok : true , message : 'applied' } ] } } )
}
case 'fips.apply-seed-anchors' : {
return res . json ( { result : { applied : 2 , results : [
{ npub : 'npub1' + randomHex ( 28 ) , ok : true , message : 'connected' } ,
{ npub : 'npub1' + randomHex ( 28 ) , ok : true , message : 'connected' } ,
] } } )
}
case 'fips.reconnect' : {
return res . json ( { result : { success : true , anchor _connected : true , authenticated _peer _count : 5 } } )
}
case 'fips.install' : {
return res . json ( { result : { success : true , installed : true , version : '0.4.2' } } )
}
2026-03-22 03:30:21 +00:00
case 'network.diagnostics' : {
return res . json ( {
result : {
wan _ip : '82.14.203.47' ,
nat _type : 'Full Cone' ,
upnp _available : true ,
tor _connected : true ,
wifi _count : 3 ,
} ,
} )
}
case 'network.list-interfaces' : {
return res . json ( {
result : {
interfaces : [
{ name : 'eth0' , type : 'ethernet' , state : 'up' , mac : 'a8:a1:59:3c:f2:10' , ipv4 : [ '192.168.1.228/24' ] } ,
{ name : 'wlan0' , type : 'wifi' , state : 'up' , mac : 'dc:a6:32:12:ab:cd' , ipv4 : [ '192.168.1.230/24' ] } ,
{ name : 'lo' , type : 'loopback' , state : 'up' , mac : '00:00:00:00:00:00' , ipv4 : [ '127.0.0.1/8' ] } ,
{ name : 'podman0' , type : 'bridge' , state : 'up' , mac : '2e:f4:8a:11:22:33' , ipv4 : [ '10.89.0.1/16' ] } ,
{ name : 'tailscale0' , type : 'tunnel' , state : 'up' , mac : '' , ipv4 : [ '100.82.97.63/32' ] } ,
] ,
} ,
} )
}
case 'network.scan-wifi' : {
return res . json ( {
result : {
networks : [
{ ssid : 'HomeNetwork' , signal : 92 , security : 'WPA2' } ,
{ ssid : 'Neighbor-5G' , signal : 45 , security : 'WPA3' } ,
{ ssid : 'CoffeeShop' , signal : 30 , security : 'Open' } ,
] ,
} ,
} )
}
case 'network.configure-wifi' : {
console . log ( ` [Network] Connecting to WiFi: ${ params ? . ssid } ` )
return res . json ( { result : { success : true } } )
}
case 'network.dns-status' : {
2026-07-15 03:49:23 -04:00
const dns = mockState . dns || { provider : 'system' , servers : [ '1.1.1.1' , '9.9.9.9' ] , doh _enabled : false }
2026-03-22 03:30:21 +00:00
return res . json ( {
result : {
2026-07-15 03:49:23 -04:00
provider : dns . provider ,
servers : dns . servers ,
doh _enabled : dns . doh _enabled ,
2026-03-22 03:30:21 +00:00
doh _url : null ,
2026-07-15 03:49:23 -04:00
resolv _conf _servers : dns . servers ,
2026-03-22 03:30:21 +00:00
} ,
} )
}
case 'network.configure-dns' : {
2026-07-15 03:49:23 -04:00
const dnsProviders = {
system : [ '192.168.4.1' ] ,
cloudflare : [ '1.1.1.1' , '1.0.0.1' ] ,
google : [ '8.8.8.8' , '8.8.4.4' ] ,
quad9 : [ '9.9.9.9' , '149.112.112.112' ] ,
mullvad : [ '194.242.2.2' ] ,
}
const provider = params ? . provider || 'system'
const servers = provider === 'custom'
? ( Array . isArray ( params ? . servers ) ? params . servers : [ ] )
: ( dnsProviders [ provider ] || dnsProviders . system )
const doh _enabled = [ 'cloudflare' , 'google' , 'quad9' , 'mullvad' ] . includes ( provider )
mockState . dns = { provider , servers , doh _enabled }
console . log ( ` [Network] DNS configured: ${ provider } → ${ servers . join ( ', ' ) } ` )
return res . json ( { result : { provider , servers , doh _enabled } } )
2026-03-22 03:30:21 +00:00
}
2026-03-09 13:03:53 +00:00
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 } } )
}
2026-03-22 03:30:21 +00:00
case 'router.list-forwards' : {
return res . json ( {
result : {
forwards : [
{ id : 'fwd-1' , name : 'Bitcoin P2P' , external _port : 8333 , internal _port : 8333 , protocol : 'tcp' , enabled : true } ,
{ id : 'fwd-2' , name : 'LND gRPC' , external _port : 10009 , internal _port : 10009 , protocol : 'tcp' , enabled : true } ,
] ,
} ,
} )
}
2026-03-09 13:03:53 +00:00
// =========================================================================
// Tor & Peer Networking
// =========================================================================
2026-03-22 03:30:21 +00:00
case 'tor.list-services' : {
2026-07-15 05:17:01 -04:00
mockState . torServices = mockState . torServices || defaultTorServices ( )
2026-03-22 03:30:21 +00:00
return res . json ( {
result : {
tor _running : true ,
2026-07-15 05:17:01 -04:00
services : mockState . torServices ,
2026-03-22 03:30:21 +00:00
} ,
} )
}
2026-07-15 05:17:01 -04:00
case 'tor.create-service' : {
mockState . torServices = mockState . torServices || defaultTorServices ( )
const name = params ? . name
if ( ! name ) {
return res . json ( { error : { code : - 1 , message : 'Missing name' } } )
}
if ( mockState . torServices . some ( s => s . name === name ) ) {
return res . json ( { error : { code : - 1 , message : ` Service ' ${ name } ' already exists ` } } )
}
const stem = ` ${ name . toLowerCase ( ) . replace ( /[^a-z0-9]/g , '' ) } demo `
const onion = ( stem + 'x7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioadmnopqrstuv' ) . slice ( 0 , 56 ) + '.onion'
const svc = { name , local _port : params ? . local _port || 8080 , onion _address : onion , enabled : true , unauthenticated : false , protocol : false }
mockState . torServices = [ ... mockState . torServices , svc ]
console . log ( ` [Tor] Created hidden service: ${ name } → ${ onion } ` )
return res . json ( { result : { created : true , name , onion _address : onion } } )
}
case 'tor.delete-service' : {
mockState . torServices = mockState . torServices || defaultTorServices ( )
const name = params ? . name
if ( name === 'archipelago' ) {
return res . json ( { error : { code : - 1 , message : "Cannot delete the node's own Tor service" } } )
}
if ( ! mockState . torServices . some ( s => s . name === name ) ) {
return res . json ( { error : { code : - 1 , message : ` Service ' ${ name } ' not found ` } } )
}
mockState . torServices = mockState . torServices . filter ( s => s . name !== name )
return res . json ( { result : { deleted : true , name } } )
}
2026-03-09 13:03:53 +00:00
case 'node.tor-address' : {
return res . json ( {
result : {
tor _address : 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion' ,
} ,
} )
}
2026-03-08 00:27:34 +00:00
case 'node-list-peers' : {
2026-03-09 13:03:53 +00:00
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' } ,
2026-07-14 06:40:26 -04:00
{ did : 'did:key:z6MkfV2sQpXm4d8YtR1nWc7uHb3eKj9gLa5xPzD6oTiN8rEw' , onion : 'disc3mn04pq15rs26tu37vw48xy59za60bc71de.onion' , pubkey : 'disc3pub' , node _address : '192.168.1.72' } ,
{ did : 'did:key:z6MkrJ8pWx2yNc5vT9qLb4eHu7dKf1gMa3sPzE6oXiQ8nRvw' , onion : 'disc4fg82hi93jk04lm15no26pq37rs48tu59vw.onion' , pubkey : 'disc4pub' , node _address : '100.72.19.44' } ,
{ did : 'did:key:z6MkhT4wQn8xPc2vL6sRb9eYu3dJf7gKa1mNzD5oWiE8tXvq' , onion : 'disc5xy60za71bc82de93fg04hi15jk26lm37no.onion' , pubkey : 'disc5pub' , node _address : '100.101.7.23' } ,
2026-03-09 13:03:53 +00:00
] ,
} ,
} )
}
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' : {
2026-03-18 19:24:52 +00:00
return res . json ( {
result : [
{
id : 'notif-1' ,
type : 'info' ,
title : 'Bitcoin Core synced' ,
message : 'Blockchain fully synced at block 892,451. IBD complete.' ,
timestamp : new Date ( Date . now ( ) - 1800000 ) . toISOString ( ) ,
read : false ,
} ,
{
id : 'notif-2' ,
type : 'success' ,
title : 'LND channel opened' ,
message : 'Channel opened with ACINQ (500,000 sats capacity). 6 confirmations received.' ,
timestamp : new Date ( Date . now ( ) - 7200000 ) . toISOString ( ) ,
read : false ,
} ,
{
id : 'notif-3' ,
type : 'warning' ,
title : 'Disk usage above 80%' ,
message : 'Storage at 82% capacity. Consider pruning old blockchain data or expanding storage.' ,
timestamp : new Date ( Date . now ( ) - 14400000 ) . toISOString ( ) ,
read : true ,
} ,
{
id : 'notif-4' ,
type : 'info' ,
title : 'System update available' ,
message : 'Archipelago v0.1.1 is available. Review changelog before updating.' ,
timestamp : new Date ( Date . now ( ) - 86400000 ) . toISOString ( ) ,
read : true ,
} ,
{
id : 'notif-5' ,
type : 'success' ,
title : 'Fedimint guardian connected' ,
message : 'Guardian consensus achieved. Mint is operational with 3/4 guardians online.' ,
timestamp : new Date ( Date . now ( ) - 43200000 ) . toISOString ( ) ,
read : false ,
} ,
] ,
} )
2026-03-07 22:36:45 +00:00
}
2026-03-17 01:32:02 +00:00
// =====================================================================
// Federation (multi-node clusters)
// =====================================================================
feat(demo): network/wallet dummy data — profits, federation, VPN, nostr, visibility
- wallet.networking-profits = 5,231,978 sats (content 3,180,000 / routing
1,281,978 / relay 770,000); 6 labelled profit transactions added to the wallet
history (1-2 per type: content sale, routing fee, file/mesh relay) — labels are
production-ready.
- federation.list (the Web5 Federation container's method) now returns the 12
demo nodes (was unhandled → empty).
- vpn.status: connected WireGuard with peers + traffic.
- nostr.list-relays / nostr.get-stats: 5 relays (3 connected).
- network.get/set-visibility: interactive, persisted per demo session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:18:29 -04:00
case 'federation.list' :
2026-03-17 01:32:02 +00:00
case 'federation.list-nodes' : {
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
return res . json ( { result : { nodes : demoFederationNodes ( ) } } )
2026-03-17 01:32:02 +00:00
}
case 'federation.invite' : {
2026-07-14 08:55:02 -04:00
// trust level threads through the invite (backend parity):
// "Invite a Peer" sends observer, "Link Your Nodes" trusted.
const trust = params ? . trust _level === 'observer' ? 'observer' : 'trusted'
2026-03-17 01:32:02 +00:00
const mockCode = 'fed1:' + Buffer . from ( JSON . stringify ( {
did : 'did:key:z6MkTest228NodeInvite' ,
onion : 'self228abc2def3ghi4jkl5mno6pqr7stu8vwx.onion' ,
pubkey : 'aabbccdd' ,
token : 'mock-invite-token-' + Date . now ( ) ,
2026-07-14 08:55:02 -04:00
trust ,
2026-03-17 01:32:02 +00:00
} ) ) . toString ( 'base64url' )
return res . json ( {
result : {
code : mockCode ,
did : 'did:key:z6MkTest228NodeInvite' ,
onion : 'self228abc2def3ghi4jkl5mno6pqr7stu8vwx.onion' ,
2026-07-14 08:55:02 -04:00
trust _level : trust ,
2026-03-17 01:32:02 +00:00
} ,
} )
}
case 'federation.join' : {
console . log ( ` [Federation] Joining with code: ${ params ? . code ? . substring ( 0 , 20 ) } ... ` )
return res . json ( {
result : {
joined : true ,
node : {
did : 'did:key:z6MkNewJoinedNode' ,
onion : 'newnode123abc456def789ghi012jkl345mno6pqr.onion' ,
pubkey : 'ddeeff11' ,
trust _level : 'trusted' ,
} ,
} ,
} )
}
case 'federation.sync-state' : {
return res . json ( {
result : {
synced : 3 ,
failed : 0 ,
results : [
{ did : 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9' , status : 'synced' , apps : 4 } ,
{ did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' , status : 'synced' , apps : 3 } ,
{ did : 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb' , status : 'synced' , apps : 2 } ,
] ,
} ,
} )
}
case 'federation.set-trust' : {
console . log ( ` [Federation] Set trust: ${ params ? . did } -> ${ params ? . trust _level } ` )
return res . json ( { result : { updated : true , did : params ? . did , trust _level : params ? . trust _level } } )
}
case 'federation.remove-node' : {
console . log ( ` [Federation] Remove node: ${ params ? . did } ` )
return res . json ( { result : { removed : true , nodes _remaining : 2 } } )
}
case 'federation.deploy-app' : {
console . log ( ` [Federation] Deploy app: ${ params ? . app _id } to ${ params ? . target _did } ` )
return res . json ( { result : { deployed : true , app _id : params ? . app _id } } )
}
// =====================================================================
// DWN (Decentralized Web Node)
// =====================================================================
case 'dwn.status' : {
return res . json ( {
result : {
running : true ,
protocols _registered : 3 ,
messages _stored : 47 ,
peers _synced : 2 ,
last _sync : new Date ( Date . now ( ) - 600000 ) . toISOString ( ) ,
protocols : [
{ uri : 'https://archipelago.dev/protocols/node-identity/v1' , published : true , messages : 12 } ,
{ uri : 'https://archipelago.dev/protocols/federation/v1' , published : false , messages : 28 } ,
{ uri : 'https://archipelago.dev/protocols/app-deploy/v1' , published : false , messages : 7 } ,
] ,
} ,
} )
}
case 'dwn.sync' : {
console . log ( '[DWN] Syncing with peers...' )
return res . json ( { result : { synced : true , messages _pulled : 5 , messages _pushed : 3 } } )
}
2026-03-17 00:03:08 +00:00
// =====================================================================
// Mesh Networking (LoRa radio via Meshcore)
// =====================================================================
case 'mesh.status' : {
2026-06-17 19:21:42 -04:00
globalThis . _ _meshHeaders || = { announce _block _headers : false , receive _block _headers : true }
feat(mesh): device-detected setup modal with board images + full radio settings
Plugging in a LoRa radio now pops a global two-step setup modal on any
page: step 1 shows the ACTUAL detected board (25 board SVGs vendored
from the Meshtastic web-flasher, GPL-3.0; matched by USB product string
on native-USB boards, vid:pid heuristics for bridge chips, animated
fallback otherwise); step 2 configures with Archipelago presets — the
LoRa region pre-selected from the node's location (new lat/lon→region
mapping), channel 'archipelago', node name, and an advanced firmware
pin. Options adapt per protocol: region+channel program Meshtastic;
MeshCore shows its community frequency plan (firmware owns RF); RNode
defers to the Reticulum daemon config.
Backend: mesh.configure now accepts lora_region (validated against the
driver's region table) and device_kind (probe pin); mesh.status returns
the persisted values plus per-port USB identity (sysfs vid/pid/product)
for board identification. The Mesh Device tab gains a full settings
form (region with duty-cycle/legality hints, firmware pin, channel,
name, identity broadcast) replacing the read-only panel; the old
Mesh-page-only onboarding modal is superseded by the global flow.
Mock backend: stateful mesh config so the dev UI demos the full
detect→configure round-trip (disable mesh → modal fires with Heltec V3).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 08:40:18 -04:00
// Stateful enable/disable so the dev UI can demo the global
// "mesh device detected" setup modal: disable mesh on the Mesh page
// and the modal fires (device present but not connected).
globalThis . _ _meshCfg || = { enabled : true , lora _region : 'EU_868' , device _kind : null , channel _name : 'archipelago' , advert _name : 'archy-228' }
const mc = globalThis . _ _meshCfg
2026-03-17 00:03:08 +00:00
return res . json ( {
result : {
feat(mesh): device-detected setup modal with board images + full radio settings
Plugging in a LoRa radio now pops a global two-step setup modal on any
page: step 1 shows the ACTUAL detected board (25 board SVGs vendored
from the Meshtastic web-flasher, GPL-3.0; matched by USB product string
on native-USB boards, vid:pid heuristics for bridge chips, animated
fallback otherwise); step 2 configures with Archipelago presets — the
LoRa region pre-selected from the node's location (new lat/lon→region
mapping), channel 'archipelago', node name, and an advanced firmware
pin. Options adapt per protocol: region+channel program Meshtastic;
MeshCore shows its community frequency plan (firmware owns RF); RNode
defers to the Reticulum daemon config.
Backend: mesh.configure now accepts lora_region (validated against the
driver's region table) and device_kind (probe pin); mesh.status returns
the persisted values plus per-port USB identity (sysfs vid/pid/product)
for board identification. The Mesh Device tab gains a full settings
form (region with duty-cycle/legality hints, firmware pin, channel,
name, identity broadcast) replacing the read-only panel; the old
Mesh-page-only onboarding modal is superseded by the global flow.
Mock backend: stateful mesh config so the dev UI demos the full
detect→configure round-trip (disable mesh → modal fires with Heltec V3).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 08:40:18 -04:00
enabled : mc . enabled ,
device _type : mc . enabled ? 'Meshcore' : 'unknown' ,
2026-03-17 00:03:08 +00:00
device _path : '/dev/ttyUSB0' ,
feat(mesh): device-detected setup modal with board images + full radio settings
Plugging in a LoRa radio now pops a global two-step setup modal on any
page: step 1 shows the ACTUAL detected board (25 board SVGs vendored
from the Meshtastic web-flasher, GPL-3.0; matched by USB product string
on native-USB boards, vid:pid heuristics for bridge chips, animated
fallback otherwise); step 2 configures with Archipelago presets — the
LoRa region pre-selected from the node's location (new lat/lon→region
mapping), channel 'archipelago', node name, and an advanced firmware
pin. Options adapt per protocol: region+channel program Meshtastic;
MeshCore shows its community frequency plan (firmware owns RF); RNode
defers to the Reticulum daemon config.
Backend: mesh.configure now accepts lora_region (validated against the
driver's region table) and device_kind (probe pin); mesh.status returns
the persisted values plus per-port USB identity (sysfs vid/pid/product)
for board identification. The Mesh Device tab gains a full settings
form (region with duty-cycle/legality hints, firmware pin, channel,
name, identity broadcast) replacing the read-only panel; the old
Mesh-page-only onboarding modal is superseded by the global flow.
Mock backend: stateful mesh config so the dev UI demos the full
detect→configure round-trip (disable mesh → modal fires with Heltec V3).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 08:40:18 -04:00
device _connected : mc . enabled ,
firmware _version : mc . enabled ? '2.3.1' : null ,
self _node _id : mc . enabled ? 42 : null ,
self _advert _name : mc . advert _name ,
peer _count : mc . enabled ? 4 : 0 ,
channel _name : mc . channel _name ,
2026-03-17 00:03:08 +00:00
messages _sent : 23 ,
messages _received : 47 ,
detected _devices : [ '/dev/ttyUSB0' ] ,
feat(mesh): device-detected setup modal with board images + full radio settings
Plugging in a LoRa radio now pops a global two-step setup modal on any
page: step 1 shows the ACTUAL detected board (25 board SVGs vendored
from the Meshtastic web-flasher, GPL-3.0; matched by USB product string
on native-USB boards, vid:pid heuristics for bridge chips, animated
fallback otherwise); step 2 configures with Archipelago presets — the
LoRa region pre-selected from the node's location (new lat/lon→region
mapping), channel 'archipelago', node name, and an advanced firmware
pin. Options adapt per protocol: region+channel program Meshtastic;
MeshCore shows its community frequency plan (firmware owns RF); RNode
defers to the Reticulum daemon config.
Backend: mesh.configure now accepts lora_region (validated against the
driver's region table) and device_kind (probe pin); mesh.status returns
the persisted values plus per-port USB identity (sysfs vid/pid/product)
for board identification. The Mesh Device tab gains a full settings
form (region with duty-cycle/legality hints, firmware pin, channel,
name, identity broadcast) replacing the read-only panel; the old
Mesh-page-only onboarding modal is superseded by the global flow.
Mock backend: stateful mesh config so the dev UI demos the full
detect→configure round-trip (disable mesh → modal fires with Heltec V3).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 08:40:18 -04:00
device _present : true ,
detected _device _info : [
{ path : '/dev/ttyUSB0' , vid : '10c4' , pid : 'ea60' , product : 'HELTEC LoRa 32 V3' , manufacturer : 'Heltec' } ,
] ,
lora _region : mc . lora _region ,
device _kind : mc . device _kind ,
region : mc . enabled ? mc . lora _region : null ,
2026-06-17 19:21:42 -04:00
announce _block _headers : globalThis . _ _meshHeaders . announce _block _headers ,
receive _block _headers : globalThis . _ _meshHeaders . receive _block _headers ,
2026-03-17 00:03:08 +00:00
} ,
} )
}
case 'mesh.peers' : {
return res . json ( {
result : {
peers : [
{
contact _id : 1 ,
advert _name : 'archy-198' ,
did : 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9' ,
pubkey _hex : 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2' ,
rssi : - 67 ,
snr : 9.5 ,
last _heard : new Date ( Date . now ( ) - 30000 ) . toISOString ( ) ,
hops : 0 ,
} ,
{
contact _id : 2 ,
advert _name : 'satoshi-relay' ,
did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' ,
pubkey _hex : 'f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5' ,
rssi : - 82 ,
snr : 4.2 ,
last _heard : new Date ( Date . now ( ) - 120000 ) . toISOString ( ) ,
hops : 1 ,
} ,
{
contact _id : 3 ,
advert _name : 'mountain-node' ,
did : null ,
pubkey _hex : null ,
rssi : - 95 ,
snr : 1.8 ,
last _heard : new Date ( Date . now ( ) - 600000 ) . toISOString ( ) ,
hops : 2 ,
} ,
{
contact _id : 4 ,
advert _name : 'bunker-alpha' ,
did : 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb' ,
pubkey _hex : 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4' ,
rssi : - 74 ,
snr : 7.1 ,
last _heard : new Date ( Date . now ( ) - 45000 ) . toISOString ( ) ,
hops : 0 ,
} ,
] ,
count : 4 ,
} ,
} )
}
case 'mesh.messages' : {
const limit = params ? . limit || 100
const now = Date . now ( )
const allMessages = [
feat: Phase 3 Week 7 — typed message UI, session badges, rich chat cards
Frontend store (mesh.ts):
- Add typed message interfaces: InvoiceData, AlertData, CoordinateData,
SessionStatus, AlertStatus, MeshMessageTypeLabel
- New actions: sendInvoice, sendCoordinate, sendAlert, getSessionStatus,
rotatePrekeys
Mesh.vue UI:
- Typed message rendering in chat bubbles:
- Invoice: orange card with sats amount, memo, bolt11 preview, paid badge
- Alert: red card (emergency/dead_man) or blue (status), signed badge,
GPS link to OpenStreetMap
- Coordinate: blue card with lat/lng, label, OSM map link
- Block header: purple inline with chain icon
- Session badge in chat header: green shield (Double Ratchet),
yellow (static encryption), gray (none)
- Session status fetched on peer selection via mesh.session-status RPC
Mock backend:
- Messages now include message_type and typed_payload fields
- Mix of text, invoice (paid + unpaid), alert (emergency + status),
coordinate, and block_header messages for testing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 02:34:37 +00:00
{ 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...' } } ,
2026-03-17 00:03:08 +00:00
]
return res . json ( {
result : {
messages : allMessages . slice ( 0 , limit ) ,
count : allMessages . length ,
} ,
} )
}
case 'mesh.send' : {
const contactId = params ? . contact _id
const message = params ? . message || ''
const peer = [
{ id : 1 , name : 'archy-198' , encrypted : true } ,
{ id : 2 , name : 'satoshi-relay' , encrypted : true } ,
{ id : 3 , name : 'mountain-node' , encrypted : false } ,
{ id : 4 , name : 'bunker-alpha' , encrypted : true } ,
] . find ( p => p . id === contactId )
console . log ( ` [Mesh] Send to ${ peer ? . name || contactId } : ${ message } ` )
return res . json ( {
result : {
sent : true ,
message _id : Math . floor ( Math . random ( ) * 10000 ) + 100 ,
encrypted : peer ? . encrypted ? ? false ,
} ,
} )
}
case 'mesh.broadcast' : {
console . log ( '[Mesh] Broadcasting identity over LoRa' )
return res . json ( { result : { broadcast : true } } )
}
case 'mesh.configure' : {
console . log ( ` [Mesh] Configure: ` , params )
2026-06-17 19:21:42 -04:00
globalThis . _ _meshHeaders || = { announce _block _headers : false , receive _block _headers : true }
if ( params && typeof params . announce _block _headers === 'boolean' ) globalThis . _ _meshHeaders . announce _block _headers = params . announce _block _headers
if ( params && typeof params . receive _block _headers === 'boolean' ) globalThis . _ _meshHeaders . receive _block _headers = params . receive _block _headers
feat(mesh): device-detected setup modal with board images + full radio settings
Plugging in a LoRa radio now pops a global two-step setup modal on any
page: step 1 shows the ACTUAL detected board (25 board SVGs vendored
from the Meshtastic web-flasher, GPL-3.0; matched by USB product string
on native-USB boards, vid:pid heuristics for bridge chips, animated
fallback otherwise); step 2 configures with Archipelago presets — the
LoRa region pre-selected from the node's location (new lat/lon→region
mapping), channel 'archipelago', node name, and an advanced firmware
pin. Options adapt per protocol: region+channel program Meshtastic;
MeshCore shows its community frequency plan (firmware owns RF); RNode
defers to the Reticulum daemon config.
Backend: mesh.configure now accepts lora_region (validated against the
driver's region table) and device_kind (probe pin); mesh.status returns
the persisted values plus per-port USB identity (sysfs vid/pid/product)
for board identification. The Mesh Device tab gains a full settings
form (region with duty-cycle/legality hints, firmware pin, channel,
name, identity broadcast) replacing the read-only panel; the old
Mesh-page-only onboarding modal is superseded by the global flow.
Mock backend: stateful mesh config so the dev UI demos the full
detect→configure round-trip (disable mesh → modal fires with Heltec V3).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 08:40:18 -04:00
// Stateful radio settings (see mesh.status) — lets the dev UI demo
// the detected-device modal + the Device panel settings round-trip.
globalThis . _ _meshCfg || = { enabled : true , lora _region : 'EU_868' , device _kind : null , channel _name : 'archipelago' , advert _name : 'archy-228' }
const cfg = globalThis . _ _meshCfg
if ( params && typeof params . enabled === 'boolean' ) cfg . enabled = params . enabled
if ( params && typeof params . lora _region === 'string' ) cfg . lora _region = params . lora _region || null
if ( params && typeof params . device _kind === 'string' ) cfg . device _kind = params . device _kind === 'auto' ? null : params . device _kind
if ( params && typeof params . channel _name === 'string' ) cfg . channel _name = params . channel _name
if ( params && typeof params . advert _name === 'string' ) cfg . advert _name = params . advert _name
return res . json ( { result : { configured : true , enabled : cfg . enabled , lora _region : cfg . lora _region , device _kind : cfg . device _kind , ... globalThis . _ _meshHeaders } } )
2026-03-17 00:03:08 +00:00
}
2026-03-17 02:23:30 +00:00
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 ,
} ,
} )
}
2026-03-22 03:30:21 +00:00
case 'mesh.deadman-status' : {
return res . json ( {
result : {
dead _man _enabled : false ,
dead _man _interval _secs : 21600 ,
time _remaining _secs : 21600 ,
triggered : false ,
has _gps : false ,
emergency _contacts : 2 ,
} ,
} )
}
case 'mesh.deadman-configure' : {
const { enabled , interval _secs } = params || { }
console . log ( ` [Mesh] Deadman configured: enabled= ${ enabled } , interval= ${ interval _secs } ` )
return res . json ( {
result : {
dead _man _enabled : enabled ? ? false ,
dead _man _interval _secs : interval _secs ? ? 21600 ,
time _remaining _secs : interval _secs ? ? 21600 ,
triggered : false ,
has _gps : false ,
emergency _contacts : 2 ,
} ,
} )
}
case 'mesh.deadman-checkin' : {
return res . json ( {
result : {
checked _in : true ,
time _remaining _secs : 21600 ,
} ,
} )
}
case 'mesh.block-headers' : {
return res . json ( {
result : {
headers : [
{ height : 893421 , hash : '0000000000000000000234a6b12dc03e5c4f7e891d2f34b5a678cd9012345678' , timestamp : new Date ( Date . now ( ) - 600000 ) . toISOString ( ) } ,
{ height : 893420 , hash : '00000000000000000001bc7d89ef01234567890abcdef1234567890abcdef12' , timestamp : new Date ( Date . now ( ) - 1200000 ) . toISOString ( ) } ,
{ height : 893419 , hash : '00000000000000000003ef4a56789012bcdef34567890abcdef1234567890ab' , timestamp : new Date ( Date . now ( ) - 1800000 ) . toISOString ( ) } ,
] ,
latest _height : 893421 ,
count : 3 ,
} ,
} )
}
case 'mesh.relay-tx' : {
return res . json ( {
result : {
request _id : Math . floor ( Math . random ( ) * 10000 ) ,
queued : true ,
tx _hex _len : ( params ? . tx _hex || '' ) . length ,
} ,
} )
}
case 'mesh.relay-status' : {
return res . json ( {
result : {
relayed : true ,
pending _count : 0 ,
status : 'confirmed' ,
txid : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
} ,
} )
}
case 'mesh.relay-lightning' : {
return res . json ( {
result : {
request _id : Math . floor ( Math . random ( ) * 10000 ) ,
queued : true ,
amount _sats : params ? . amount _sats || 0 ,
} ,
} )
}
2026-03-17 00:03:08 +00:00
// =====================================================================
// Transport Layer (unified routing: mesh > lan > tor)
// =====================================================================
case 'transport.status' : {
return res . json ( {
result : {
transports : [
{ kind : 'mesh' , available : true } ,
{ kind : 'lan' , available : true } ,
{ kind : 'tor' , available : true } ,
] ,
mesh _only : false ,
peer _count : 5 ,
} ,
} )
}
case 'transport.peers' : {
return res . json ( {
result : {
peers : [
{
did : 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9' ,
pubkey _hex : 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2' ,
name : 'archy-198' ,
trust _level : 'trusted' ,
mesh _contact _id : 1 ,
lan _address : '192.168.1.198:5678' ,
onion _address : 'peer1abc2def3ghi4jkl5mno6pqr7stu8vwx9yz.onion' ,
preferred _transport : 'lan' ,
available _transports : [ 'mesh' , 'lan' , 'tor' ] ,
last _seen : new Date ( Date . now ( ) - 30000 ) . toISOString ( ) ,
} ,
{
did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' ,
pubkey _hex : 'f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5' ,
name : 'satoshi-relay' ,
trust _level : 'trusted' ,
mesh _contact _id : 2 ,
lan _address : null ,
onion _address : 'peer2xyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba.onion' ,
preferred _transport : 'mesh' ,
available _transports : [ 'mesh' , 'tor' ] ,
last _seen : new Date ( Date . now ( ) - 120000 ) . toISOString ( ) ,
} ,
{
did : 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb' ,
pubkey _hex : 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4' ,
name : 'bunker-alpha' ,
trust _level : 'observer' ,
mesh _contact _id : 4 ,
lan _address : null ,
onion _address : null ,
preferred _transport : 'mesh' ,
available _transports : [ 'mesh' ] ,
last _seen : new Date ( Date . now ( ) - 45000 ) . toISOString ( ) ,
} ,
{
did : 'did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG' ,
pubkey _hex : 'd4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5' ,
name : 'office-node' ,
trust _level : 'trusted' ,
mesh _contact _id : null ,
lan _address : '192.168.1.42:5678' ,
onion _address : 'peer4mno6pqr7stu8vwx9yzabc2def3ghi4jkl5.onion' ,
preferred _transport : 'lan' ,
available _transports : [ 'lan' , 'tor' ] ,
last _seen : new Date ( Date . now ( ) - 60000 ) . toISOString ( ) ,
} ,
{
did : 'did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp7NQD5EjEREWh' ,
pubkey _hex : 'e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6' ,
name : 'remote-cabin' ,
trust _level : 'trusted' ,
mesh _contact _id : null ,
lan _address : null ,
onion _address : 'peer5xyz9abc2def3ghi4jkl5mno6pqr7stu8vw.onion' ,
preferred _transport : 'tor' ,
available _transports : [ 'tor' ] ,
last _seen : new Date ( Date . now ( ) - 300000 ) . toISOString ( ) ,
} ,
] ,
} ,
} )
}
case 'transport.send' : {
const targetDid = params ? . did
console . log ( ` [Transport] Send to ${ targetDid } via best transport ` )
return res . json ( {
result : {
sent : true ,
transport _used : 'mesh' ,
did : targetDid ,
} ,
} )
}
case 'transport.set-mode' : {
const meshOnly = params ? . mesh _only ? ? false
console . log ( ` [Transport] Set mesh_only mode: ${ meshOnly } ` )
return res . json ( { result : { mesh _only : meshOnly , configured : true } } )
}
2026-03-18 21:06:14 +00:00
// =====================================================================
// LND / Lightning
// =====================================================================
case 'lnd.getinfo' : {
return res . json ( {
result : {
alias : 'archy-signet' ,
color : '#f7931a' ,
num _active _channels : 4 ,
num _inactive _channels : 1 ,
num _pending _channels : 1 ,
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
block _height : walletState . block _height ,
2026-03-18 21:06:14 +00:00
synced _to _chain : true ,
synced _to _graph : true ,
version : '0.17.4-beta' ,
identity _pubkey : '03a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
chains : [ { chain : 'bitcoin' , network : 'signet' } ] ,
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
balance _sats : walletState . onchain _sats ,
channel _balance _sats : walletState . channel _sats ,
2026-03-18 21:06:14 +00:00
} ,
} )
}
case 'lnd.gettransactions' : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
const pending = walletState . transactions . filter ( tx => tx . direction === 'incoming' && tx . num _confirmations < 3 ) . length
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
transactions : walletState . transactions ,
incoming _pending _count : pending ,
2026-03-18 21:06:14 +00:00
} ,
} )
}
case 'lnd.channelbalance' : {
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
local _balance : { sat : walletState . channel _sats } ,
2026-03-18 21:06:14 +00:00
remote _balance : { sat : 11750000 } ,
pending _open _local _balance : { sat : 500000 } ,
} ,
} )
}
case 'lnd.walletbalance' : {
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
total _balance : walletState . onchain _sats + 100000 ,
confirmed _balance : walletState . onchain _sats ,
2026-03-18 21:06:14 +00:00
unconfirmed _balance : 100000 ,
} ,
} )
}
case 'lnd.listchannels' : {
feat(setup): Zeus lightning journey — fund-wallet step, IBD timer + finish-setup toast, channel suggestions
Every Lightning setup (Open a Shop, Accept Payments, Run a Lightning Node)
gains a guided path to a working channel:
- New "Fund Your Bitcoin Wallet" step, gated on the blockchain finishing
its initial sync: while syncing it shows a live progress bar with a
counting-down time-remaining estimate; once synced it shows the on-chain
balance and a "Fund Wallet" button that opens the receive modal with a
fresh address, QR, and the Zeus channel limits (min 150,000 / max
1,500,000 sats) noted.
- When the sync finishes while a Lightning setup is mid-flight, a toast
pops with a "Finish setup" link straight back to the wizard (toasts now
support action links). If several setups are in flight, one is chosen —
the shared steps complete the lightning part of any of them.
- Channel steps are Zeus-branded (logo + copy) and land on the Lightning
Channels screen, which now carries an "Open a channel with Zeus" card
that prefills the open-channel modal with the Olympus peer URI, 150k
sats, and private-channel checked. "Get Zeus" links to zeusln.com.
- Setup completion now actually requires walking the manual steps (fund,
open channel, configure) — previously any step whose app was installed
was silently auto-ticked and running apps marked the whole goal done.
- Completion CTAs now go to the app you just set up ("Go to my shop
(BTCPay)" etc.) instead of the generic services list; iframe apps open
in the on-top app overlay.
- On-chain send modal gains a "Send all funds" sweep toggle, backed by
LND's send_all on the backend (amount no longer required when sweeping).
- Demo/mock: bitcoin.getinfo now returns the block_height/sync_progress
contract the UI reads and simulates a ~90s IBD ramp per visitor so the
timer, toast, and fund flow can all be demoed live; channel list data
fixed (status/channel_point/liquidity totals — the panel previously
crashed on the missing status field); sendcoins supports send_all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:49:09 -04:00
// Shape matches the real backend: status + channel_point are required
// by the channels panel; totals feed the liquidity summary tiles.
const channels = [
{ chan _id : '840921088114688' , remote _pubkey : '031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581' , capacity : 1500000 , local _balance : 950000 , remote _balance : 550000 , active : true , status : 'active' , channel _point : randomHex ( 32 ) + ':0' , peer _alias : 'Olympus by ZEUS' } ,
{ chan _id : '840921088114689' , remote _pubkey : '03abcdef12345678901234567890123456789012345678901234567890abcdef12' , capacity : 2000000 , local _balance : 1200000 , remote _balance : 800000 , active : true , status : 'active' , channel _point : randomHex ( 32 ) + ':1' , peer _alias : 'WalletOfSatoshi' } ,
{ chan _id : '840921088114690' , remote _pubkey : '02fedcba98765432109876543210987654321098765432109876543210fedcba98' , capacity : 10000000 , local _balance : 4500000 , remote _balance : 5500000 , active : true , status : 'active' , channel _point : randomHex ( 32 ) + ':0' , peer _alias : 'Voltage' } ,
{ chan _id : '840921088114691' , remote _pubkey : '03456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123' , capacity : 3000000 , local _balance : 100000 , remote _balance : 2900000 , active : false , status : 'inactive' , channel _point : randomHex ( 32 ) + ':0' , peer _alias : 'Kraken' } ,
]
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
feat(setup): Zeus lightning journey — fund-wallet step, IBD timer + finish-setup toast, channel suggestions
Every Lightning setup (Open a Shop, Accept Payments, Run a Lightning Node)
gains a guided path to a working channel:
- New "Fund Your Bitcoin Wallet" step, gated on the blockchain finishing
its initial sync: while syncing it shows a live progress bar with a
counting-down time-remaining estimate; once synced it shows the on-chain
balance and a "Fund Wallet" button that opens the receive modal with a
fresh address, QR, and the Zeus channel limits (min 150,000 / max
1,500,000 sats) noted.
- When the sync finishes while a Lightning setup is mid-flight, a toast
pops with a "Finish setup" link straight back to the wizard (toasts now
support action links). If several setups are in flight, one is chosen —
the shared steps complete the lightning part of any of them.
- Channel steps are Zeus-branded (logo + copy) and land on the Lightning
Channels screen, which now carries an "Open a channel with Zeus" card
that prefills the open-channel modal with the Olympus peer URI, 150k
sats, and private-channel checked. "Get Zeus" links to zeusln.com.
- Setup completion now actually requires walking the manual steps (fund,
open channel, configure) — previously any step whose app was installed
was silently auto-ticked and running apps marked the whole goal done.
- Completion CTAs now go to the app you just set up ("Go to my shop
(BTCPay)" etc.) instead of the generic services list; iframe apps open
in the on-top app overlay.
- On-chain send modal gains a "Send all funds" sweep toggle, backed by
LND's send_all on the backend (amount no longer required when sweeping).
- Demo/mock: bitcoin.getinfo now returns the block_height/sync_progress
contract the UI reads and simulates a ~90s IBD ramp per visitor so the
timer, toast, and fund flow can all be demoed live; channel list data
fixed (status/channel_point/liquidity totals — the panel previously
crashed on the missing status field); sendcoins supports send_all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:49:09 -04:00
channels ,
total _outbound : channels . reduce ( ( s , c ) => s + c . local _balance , 0 ) ,
total _inbound : channels . reduce ( ( s , c ) => s + c . remote _balance , 0 ) ,
2026-03-18 21:06:14 +00:00
} ,
} )
}
case 'lnd.newaddress' : {
const addrType = params ? . type || 'p2wkh'
const mockAddr = addrType === 'p2tr'
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
? 'tb1p' + randomHex ( 29 )
: 'tb1q' + randomHex ( 19 )
2026-03-18 21:06:14 +00:00
return res . json ( { result : { address : mockAddr } } )
}
case 'lnd.addinvoice' :
case 'lnd.createinvoice' : {
const amt = params ? . amt || params ? . value || params ? . amount _sats || 1000
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
const rHash = randomHex ( 32 )
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
r _hash : rHash ,
payment _request : ` lnsb ${ amt } n1pjmock ${ Date . now ( ) . toString ( 36 ) } qqqxqyz5vqsp5mock ${ rHash . slice ( 0 , 20 ) } ` ,
add _index : Date . now ( ) ,
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
payment _addr : randomHex ( 32 ) ,
2026-03-18 21:06:14 +00:00
} ,
} )
}
case 'lnd.payinvoice' :
case 'lnd.sendpayment' : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
const amt = params ? . amt || params ? . amount _sats || 1000
const fee = Math . floor ( Math . random ( ) * 10 ) + 1
walletState . channel _sats = Math . max ( 0 , walletState . channel _sats - amt - fee )
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
payment _hash : randomHex ( 32 ) ,
payment _preimage : randomHex ( 32 ) ,
2026-03-18 21:06:14 +00:00
status : 'SUCCEEDED' ,
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
fee _sat : fee ,
value _sat : amt ,
2026-03-18 21:06:14 +00:00
} ,
} )
}
case 'lnd.sendcoins' : {
feat(setup): Zeus lightning journey — fund-wallet step, IBD timer + finish-setup toast, channel suggestions
Every Lightning setup (Open a Shop, Accept Payments, Run a Lightning Node)
gains a guided path to a working channel:
- New "Fund Your Bitcoin Wallet" step, gated on the blockchain finishing
its initial sync: while syncing it shows a live progress bar with a
counting-down time-remaining estimate; once synced it shows the on-chain
balance and a "Fund Wallet" button that opens the receive modal with a
fresh address, QR, and the Zeus channel limits (min 150,000 / max
1,500,000 sats) noted.
- When the sync finishes while a Lightning setup is mid-flight, a toast
pops with a "Finish setup" link straight back to the wizard (toasts now
support action links). If several setups are in flight, one is chosen —
the shared steps complete the lightning part of any of them.
- Channel steps are Zeus-branded (logo + copy) and land on the Lightning
Channels screen, which now carries an "Open a channel with Zeus" card
that prefills the open-channel modal with the Olympus peer URI, 150k
sats, and private-channel checked. "Get Zeus" links to zeusln.com.
- Setup completion now actually requires walking the manual steps (fund,
open channel, configure) — previously any step whose app was installed
was silently auto-ticked and running apps marked the whole goal done.
- Completion CTAs now go to the app you just set up ("Go to my shop
(BTCPay)" etc.) instead of the generic services list; iframe apps open
in the on-top app overlay.
- On-chain send modal gains a "Send all funds" sweep toggle, backed by
LND's send_all on the backend (amount no longer required when sweeping).
- Demo/mock: bitcoin.getinfo now returns the block_height/sync_progress
contract the UI reads and simulates a ~90s IBD ramp per visitor so the
timer, toast, and fund flow can all be demoed live; channel list data
fixed (status/channel_point/liquidity totals — the panel previously
crashed on the missing status field); sendcoins supports send_all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:49:09 -04:00
// send_all sweeps the entire on-chain balance (minus a mock fee)
const amt = params ? . send _all
? Math . max ( 0 , walletState . onchain _sats - 250 )
: ( params ? . amount || params ? . amt || 50000 )
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
walletState . onchain _sats = Math . max ( 0 , walletState . onchain _sats - amt )
const txid = randomHex ( 32 )
walletState . transactions . unshift ( {
tx _hash : txid , amount _sats : - amt , direction : 'outgoing' , num _confirmations : 0 ,
block _height : 0 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) , label : 'Sent on-chain' ,
total _fees : 250 , dest _addresses : [ params ? . addr || '' ] ,
} )
2026-03-18 21:06:14 +00:00
return res . json ( {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
result : { txid , amount : amt } ,
2026-03-18 21:06:14 +00:00
} )
}
case 'lnd.decodepayreq' : {
return res . json ( {
result : {
destination : '03a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
num _satoshis : params ? . pay _req ? . match ( /lnsb(\d+)/ ) ? . [ 1 ] || '1000' ,
description : 'Mock decoded invoice' ,
expiry : 3600 ,
timestamp : Math . floor ( Date . now ( ) / 1000 ) ,
} ,
} )
}
case 'lnd.openchannel' : {
return res . json ( {
result : {
funding _txid : Array . from ( { length : 32 } , ( ) => Math . floor ( Math . random ( ) * 256 ) . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ,
output _index : 0 ,
} ,
} )
}
case 'lnd.closechannel' : {
return res . json ( {
result : {
closing _txid : Array . from ( { length : 32 } , ( ) => Math . floor ( Math . random ( ) * 256 ) . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ,
} ,
} )
}
case 'lnd.listinvoices' : {
return res . json ( { result : { invoices : MOCK _LND _DATA . invoices } } )
}
case 'lnd.listpayments' : {
return res . json ( { result : { payments : MOCK _LND _DATA . payments } } )
}
case 'lnd.create-psbt' :
case 'lnd.finalize-psbt' :
case 'lnd.create-raw-tx' : {
return res . json ( {
result : {
psbt : 'cHNidP8BAH0CAAAA...mockPSBT' ,
txid : Array . from ( { length : 32 } , ( ) => Math . floor ( Math . random ( ) * 256 ) . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ,
} ,
} )
}
// =====================================================================
// Wallet / Ecash (Fedimint)
// =====================================================================
case 'wallet.ecash-balance' : {
2026-07-15 09:53:53 +01:00
const fedSats = ( mockState . federations || [ ] ) . reduce ( ( s , f ) => s + ( f . balance _sats || 0 ) , 0 )
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
balance _sats : walletState . ecash _sats ,
balance _msat : walletState . ecash _sats * 1000 ,
2026-07-15 09:53:53 +01:00
cashu _sats : walletState . ecash _sats ,
fedimint _sats : fedSats ,
ark _sats : walletState . ark _sats ,
total _sats : walletState . ecash _sats + fedSats + walletState . ark _sats ,
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
token _count : walletState . ecash _tokens ,
2026-03-18 21:06:14 +00:00
federations : [
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
{ federation _id : 'fed1-demo' , name : 'Archy Signet Mint' , balance _msat : walletState . ecash _sats * 1000 , gateway _active : true } ,
2026-03-18 21:06:14 +00:00
] ,
} ,
} )
}
case 'wallet.ecash-send' : {
const amt = params ? . amount _sats || 1000
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
walletState . ecash _sats = Math . max ( 0 , walletState . ecash _sats - amt )
walletState . ecash _tokens = Math . max ( 0 , walletState . ecash _tokens - 1 )
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
token : ` cashuSend_mock_ ${ amt } _ ${ Date . now ( ) . toString ( 36 ) } ` ,
amount _sats : amt ,
} ,
} )
}
case 'wallet.ecash-receive' : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
const amt = 5000
walletState . ecash _sats += amt
walletState . ecash _tokens += 1
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
amount _sats : amt ,
2026-03-18 21:06:14 +00:00
federation _id : 'fed1-demo' ,
} ,
} )
}
case 'wallet.ecash-history' : {
return res . json ( {
result : {
transactions : [
{ type : 'receive' , amount _sats : 50000 , timestamp : new Date ( Date . now ( ) - 86400000 ) . toISOString ( ) , note : 'Minted from Lightning' } ,
{ type : 'send' , amount _sats : 5000 , timestamp : new Date ( Date . now ( ) - 43200000 ) . toISOString ( ) , note : 'Sent ecash token' } ,
{ type : 'receive' , amount _sats : 10000 , timestamp : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) , note : 'Redeemed token' } ,
feat(ui): Ark wallet tab, balances, history badge + demo mocks
Ark tab in Wallet Settings (status, balances, receive address,
board/offboard, server config via wallet.ark-*), teal Ark badge in the
transactions modal, Ark row on the home wallet card (hidden until barkd
reports a balance), official bark icon, and wallet.ark-* mocks so the
public demo renders the tab.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 21:57:12 +01:00
// Ark movement in the unified history shape (see wallet.ark-history / load_ark_txs)
{ id : 'ark-1' , tx _type : 'receive' , type : 'receive' , amount _sats : 75000 , timestamp : new Date ( Date . now ( ) - 7200000 ) . toISOString ( ) , description : 'Received via Ark' , note : 'Received via Ark' , mint _url : '' , peer : '' , kind : 'ark' } ,
2026-03-18 21:06:14 +00:00
] ,
} ,
} )
}
case 'wallet.networking-profits' : {
2026-07-17 03:07:30 +01:00
// Deterministic-but-varied week of profit events for the dashboard
// chart (source/timestamp/sats mirror wallet::profits::ProfitEntry).
const profitSources = [ 'streaming_revenue' , 'content_sale' , 'routing_fee' ]
const profitNotes = [ 'Paid streaming session' , 'Ecash content sale' , 'Lightning routing fees' ]
const recent = [ ]
const nowMs = Date . now ( )
for ( let i = 0 ; i < 42 ; i ++ ) {
const daysAgo = ( i * 3 ) % 7
const hour = ( i * 5 ) % 24
recent . push ( {
source : profitSources [ i % 3 ] ,
amount _sats : 800 + ( ( i * 7919 ) % 14000 ) ,
timestamp : new Date ( nowMs - daysAgo * 86_400_000 - hour * 3_600_000 ) . toISOString ( ) ,
description : profitNotes [ i % 3 ] ,
} )
}
recent . sort ( ( a , b ) => b . timestamp . localeCompare ( a . timestamp ) )
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
feat(demo): network/wallet dummy data — profits, federation, VPN, nostr, visibility
- wallet.networking-profits = 5,231,978 sats (content 3,180,000 / routing
1,281,978 / relay 770,000); 6 labelled profit transactions added to the wallet
history (1-2 per type: content sale, routing fee, file/mesh relay) — labels are
production-ready.
- federation.list (the Web5 Federation container's method) now returns the 12
demo nodes (was unhandled → empty).
- vpn.status: connected WireGuard with peers + traffic.
- nostr.list-relays / nostr.get-stats: 5 relays (3 connected).
- network.get/set-visibility: interactive, persisted per demo session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:18:29 -04:00
total _sats : 5_231_978 ,
content _sales _sats : 3_180_000 ,
routing _fees _sats : 1_281_978 ,
2026-07-17 03:07:30 +01:00
streaming _revenue _sats : 770_000 ,
recent ,
feat(demo): network/wallet dummy data — profits, federation, VPN, nostr, visibility
- wallet.networking-profits = 5,231,978 sats (content 3,180,000 / routing
1,281,978 / relay 770,000); 6 labelled profit transactions added to the wallet
history (1-2 per type: content sale, routing fee, file/mesh relay) — labels are
production-ready.
- federation.list (the Web5 Federation container's method) now returns the 12
demo nodes (was unhandled → empty).
- vpn.status: connected WireGuard with peers + traffic.
- nostr.list-relays / nostr.get-stats: 5 relays (3 connected).
- network.get/set-visibility: interactive, persisted per demo session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:18:29 -04:00
// legacy aliases kept for older UI builds
2026-07-17 03:07:30 +01:00
relay _sats : 770_000 ,
feat(demo): network/wallet dummy data — profits, federation, VPN, nostr, visibility
- wallet.networking-profits = 5,231,978 sats (content 3,180,000 / routing
1,281,978 / relay 770,000); 6 labelled profit transactions added to the wallet
history (1-2 per type: content sale, routing fee, file/mesh relay) — labels are
production-ready.
- federation.list (the Web5 Federation container's method) now returns the 12
demo nodes (was unhandled → empty).
- vpn.status: connected WireGuard with peers + traffic.
- nostr.list-relays / nostr.get-stats: 5 relays (3 connected).
- network.get/set-visibility: interactive, persisted per demo session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:18:29 -04:00
total _earned _sats : 5_231_978 ,
total _forwarded _sats : 1_281_978 ,
forward _count : 1284 ,
2026-03-18 21:06:14 +00:00
period _days : 30 ,
feat(demo): network/wallet dummy data — profits, federation, VPN, nostr, visibility
- wallet.networking-profits = 5,231,978 sats (content 3,180,000 / routing
1,281,978 / relay 770,000); 6 labelled profit transactions added to the wallet
history (1-2 per type: content sale, routing fee, file/mesh relay) — labels are
production-ready.
- federation.list (the Web5 Federation container's method) now returns the 12
demo nodes (was unhandled → empty).
- vpn.status: connected WireGuard with peers + traffic.
- nostr.list-relays / nostr.get-stats: 5 relays (3 connected).
- network.get/set-visibility: interactive, persisted per demo session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:18:29 -04:00
daily _avg _sats : 174_399 ,
2026-03-18 21:06:14 +00:00
} ,
} )
}
case 'dev.faucet' : {
const amount = params ? . amount _sats || 1_000_000
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
const ecashAmt = Math . floor ( amount / 10 )
walletState . onchain _sats += amount
walletState . channel _sats += amount
walletState . ecash _sats += ecashAmt
walletState . ecash _tokens += 1
const txid = randomHex ( 32 )
walletState . transactions . unshift ( {
tx _hash : txid , amount _sats : amount , direction : 'incoming' , num _confirmations : 0 ,
block _height : 0 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) , label : 'Dev faucet' ,
total _fees : 0 , dest _addresses : [ ] ,
} )
console . log ( ` [Dev Faucet] + ${ amount } on-chain, + ${ amount } Lightning, + ${ ecashAmt } ecash ` )
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
onchain : { txid , amount _sats : amount } ,
lightning : { payment _hash : randomHex ( 32 ) , amount _sats : amount } ,
ecash : { token : ` cashuSend_faucet_ ${ amount } _ ${ Date . now ( ) . toString ( 36 ) } ` , amount _sats : ecashAmt } ,
message : ` Added ${ amount } sats on-chain, ${ amount } sats Lightning, ${ ecashAmt } sats ecash ` ,
2026-03-18 21:06:14 +00:00
} ,
} )
}
case 'bitcoin.getinfo' : {
feat(setup): Zeus lightning journey — fund-wallet step, IBD timer + finish-setup toast, channel suggestions
Every Lightning setup (Open a Shop, Accept Payments, Run a Lightning Node)
gains a guided path to a working channel:
- New "Fund Your Bitcoin Wallet" step, gated on the blockchain finishing
its initial sync: while syncing it shows a live progress bar with a
counting-down time-remaining estimate; once synced it shows the on-chain
balance and a "Fund Wallet" button that opens the receive modal with a
fresh address, QR, and the Zeus channel limits (min 150,000 / max
1,500,000 sats) noted.
- When the sync finishes while a Lightning setup is mid-flight, a toast
pops with a "Finish setup" link straight back to the wizard (toasts now
support action links). If several setups are in flight, one is chosen —
the shared steps complete the lightning part of any of them.
- Channel steps are Zeus-branded (logo + copy) and land on the Lightning
Channels screen, which now carries an "Open a channel with Zeus" card
that prefills the open-channel modal with the Olympus peer URI, 150k
sats, and private-channel checked. "Get Zeus" links to zeusln.com.
- Setup completion now actually requires walking the manual steps (fund,
open channel, configure) — previously any step whose app was installed
was silently auto-ticked and running apps marked the whole goal done.
- Completion CTAs now go to the app you just set up ("Go to my shop
(BTCPay)" etc.) instead of the generic services list; iframe apps open
in the on-top app overlay.
- On-chain send modal gains a "Send all funds" sweep toggle, backed by
LND's send_all on the backend (amount no longer required when sweeping).
- Demo/mock: bitcoin.getinfo now returns the block_height/sync_progress
contract the UI reads and simulates a ~90s IBD ramp per visitor so the
timer, toast, and fund flow can all be demoed live; channel list data
fixed (status/channel_point/liquidity totals — the panel previously
crashed on the missing status field); sendcoins supports send_all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:49:09 -04:00
// Demo IBD simulation: the first call of a session arms a ~90s ramp
// from 98.2% → 100% so the setup wizard can demo the live sync timer
// and the "finish setup" toast that fires when IBD completes.
// (The real backend returns { block_height, sync_progress } — a 0– 1
// fraction — which is what the frontend reads; the bitcoin-core-style
// fields are kept for any legacy consumers.)
if ( ! walletState . ibd _started _at ) walletState . ibd _started _at = Date . now ( )
const IBD _RAMP _MS = 90_000
const elapsed = Date . now ( ) - walletState . ibd _started _at
const syncProgress = Math . min ( 1 , 0.982 + 0.018 * ( elapsed / IBD _RAMP _MS ) )
const tipHeight = 892451
const height = Math . round ( tipHeight * syncProgress )
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
chain : 'signet' ,
feat(setup): Zeus lightning journey — fund-wallet step, IBD timer + finish-setup toast, channel suggestions
Every Lightning setup (Open a Shop, Accept Payments, Run a Lightning Node)
gains a guided path to a working channel:
- New "Fund Your Bitcoin Wallet" step, gated on the blockchain finishing
its initial sync: while syncing it shows a live progress bar with a
counting-down time-remaining estimate; once synced it shows the on-chain
balance and a "Fund Wallet" button that opens the receive modal with a
fresh address, QR, and the Zeus channel limits (min 150,000 / max
1,500,000 sats) noted.
- When the sync finishes while a Lightning setup is mid-flight, a toast
pops with a "Finish setup" link straight back to the wizard (toasts now
support action links). If several setups are in flight, one is chosen —
the shared steps complete the lightning part of any of them.
- Channel steps are Zeus-branded (logo + copy) and land on the Lightning
Channels screen, which now carries an "Open a channel with Zeus" card
that prefills the open-channel modal with the Olympus peer URI, 150k
sats, and private-channel checked. "Get Zeus" links to zeusln.com.
- Setup completion now actually requires walking the manual steps (fund,
open channel, configure) — previously any step whose app was installed
was silently auto-ticked and running apps marked the whole goal done.
- Completion CTAs now go to the app you just set up ("Go to my shop
(BTCPay)" etc.) instead of the generic services list; iframe apps open
in the on-top app overlay.
- On-chain send modal gains a "Send all funds" sweep toggle, backed by
LND's send_all on the backend (amount no longer required when sweeping).
- Demo/mock: bitcoin.getinfo now returns the block_height/sync_progress
contract the UI reads and simulates a ~90s IBD ramp per visitor so the
timer, toast, and fund flow can all be demoed live; channel list data
fixed (status/channel_point/liquidity totals — the panel previously
crashed on the missing status field); sendcoins supports send_all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:49:09 -04:00
block _height : height ,
sync _progress : syncProgress ,
blocks : height ,
headers : tipHeight ,
2026-03-18 21:06:14 +00:00
bestblockhash : 'a1b2c3d4e5f6' + '0' . repeat ( 58 ) ,
difficulty : 0.001126515290698186 ,
mediantime : Math . floor ( Date . now ( ) / 1000 ) - 300 ,
feat(setup): Zeus lightning journey — fund-wallet step, IBD timer + finish-setup toast, channel suggestions
Every Lightning setup (Open a Shop, Accept Payments, Run a Lightning Node)
gains a guided path to a working channel:
- New "Fund Your Bitcoin Wallet" step, gated on the blockchain finishing
its initial sync: while syncing it shows a live progress bar with a
counting-down time-remaining estimate; once synced it shows the on-chain
balance and a "Fund Wallet" button that opens the receive modal with a
fresh address, QR, and the Zeus channel limits (min 150,000 / max
1,500,000 sats) noted.
- When the sync finishes while a Lightning setup is mid-flight, a toast
pops with a "Finish setup" link straight back to the wizard (toasts now
support action links). If several setups are in flight, one is chosen —
the shared steps complete the lightning part of any of them.
- Channel steps are Zeus-branded (logo + copy) and land on the Lightning
Channels screen, which now carries an "Open a channel with Zeus" card
that prefills the open-channel modal with the Olympus peer URI, 150k
sats, and private-channel checked. "Get Zeus" links to zeusln.com.
- Setup completion now actually requires walking the manual steps (fund,
open channel, configure) — previously any step whose app was installed
was silently auto-ticked and running apps marked the whole goal done.
- Completion CTAs now go to the app you just set up ("Go to my shop
(BTCPay)" etc.) instead of the generic services list; iframe apps open
in the on-top app overlay.
- On-chain send modal gains a "Send all funds" sweep toggle, backed by
LND's send_all on the backend (amount no longer required when sweeping).
- Demo/mock: bitcoin.getinfo now returns the block_height/sync_progress
contract the UI reads and simulates a ~90s IBD ramp per visitor so the
timer, toast, and fund flow can all be demoed live; channel list data
fixed (status/channel_point/liquidity totals — the panel previously
crashed on the missing status field); sendcoins supports send_all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:49:09 -04:00
verificationprogress : syncProgress ,
2026-03-18 21:06:14 +00:00
chainwork : '000000000000000000000000000000000000000000000000000000000001a2b3' ,
size _on _disk : 210_000_000 ,
pruned : false ,
network : 'signet' ,
} ,
} )
}
// =====================================================================
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
// Analytics / Telemetry
// =====================================================================
case 'analytics.get-status' : {
return res . json ( { result : { enabled : mockState . analyticsEnabled || false , description : 'Anonymous aggregate statistics. No personal data collected.' } } )
}
case 'analytics.enable' : {
mockState . analyticsEnabled = true
return res . json ( { result : { enabled : true } } )
}
case 'analytics.disable' : {
mockState . analyticsEnabled = false
return res . json ( { result : { enabled : false } } )
}
2026-03-22 03:30:21 +00:00
case 'telemetry.fleet-status' : {
return res . json ( {
result : {
nodes : [
{
node _id : 'archy-228' ,
version : '0.1.0' ,
uptime _secs : 604800 ,
cpu _cores : 4 ,
cpu _pct : + ( 15 + Math . random ( ) * 20 ) . toFixed ( 1 ) ,
mem _pct : + ( 38 + Math . random ( ) * 10 ) . toFixed ( 1 ) ,
disk _pct : 34.2 ,
container _count : 12 ,
running _count : 10 ,
federation _peers : 4 ,
recent _alerts : [ ] ,
containers : [
{ id : 'bitcoin' , state : 'running' , version : '27.0' } ,
{ id : 'lnd' , state : 'running' , version : '0.18.0' } ,
{ id : 'electrs' , state : 'running' , version : '0.10.6' } ,
{ id : 'mempool' , state : 'running' , version : '3.0.0' } ,
] ,
reported _at : new Date ( ) . toISOString ( ) ,
} ,
{
node _id : 'archy-198' ,
version : '0.1.0' ,
uptime _secs : 259200 ,
cpu _cores : 4 ,
cpu _pct : + ( 8 + Math . random ( ) * 12 ) . toFixed ( 1 ) ,
mem _pct : + ( 25 + Math . random ( ) * 8 ) . toFixed ( 1 ) ,
disk _pct : 22.7 ,
container _count : 8 ,
running _count : 7 ,
federation _peers : 4 ,
recent _alerts : [ { rule : 'container_crash' , message : 'electrs restarted 2x in 1h' , timestamp : new Date ( Date . now ( ) - 7200000 ) . toISOString ( ) } ] ,
containers : [
{ id : 'bitcoin' , state : 'running' , version : '27.0' } ,
{ id : 'lnd' , state : 'running' , version : '0.18.0' } ,
{ id : 'electrs' , state : 'running' , version : '0.10.6' } ,
] ,
reported _at : new Date ( Date . now ( ) - 60000 ) . toISOString ( ) ,
} ,
{
node _id : 'arch-1' ,
version : '0.1.0' ,
uptime _secs : 172800 ,
cpu _cores : 2 ,
cpu _pct : + ( 5 + Math . random ( ) * 10 ) . toFixed ( 1 ) ,
mem _pct : + ( 45 + Math . random ( ) * 15 ) . toFixed ( 1 ) ,
disk _pct : 61.3 ,
container _count : 6 ,
running _count : 6 ,
federation _peers : 4 ,
recent _alerts : [ ] ,
containers : [
{ id : 'bitcoin' , state : 'running' , version : '27.0' } ,
{ id : 'lnd' , state : 'running' , version : '0.18.0' } ,
] ,
reported _at : new Date ( Date . now ( ) - 120000 ) . toISOString ( ) ,
} ,
{
node _id : 'arch-2' ,
version : '0.1.0' ,
uptime _secs : 86400 ,
cpu _cores : 2 ,
cpu _pct : + ( 3 + Math . random ( ) * 8 ) . toFixed ( 1 ) ,
mem _pct : + ( 30 + Math . random ( ) * 10 ) . toFixed ( 1 ) ,
disk _pct : 18.9 ,
container _count : 5 ,
running _count : 5 ,
federation _peers : 4 ,
recent _alerts : [ ] ,
containers : [
{ id : 'bitcoin' , state : 'running' , version : '27.0' } ,
] ,
reported _at : new Date ( Date . now ( ) - 300000 ) . toISOString ( ) ,
} ,
{
node _id : 'arch-3' ,
version : '0.1.0' ,
uptime _secs : 43200 ,
cpu _cores : 4 ,
cpu _pct : + ( 20 + Math . random ( ) * 15 ) . toFixed ( 1 ) ,
mem _pct : + ( 55 + Math . random ( ) * 10 ) . toFixed ( 1 ) ,
disk _pct : 47.5 ,
container _count : 10 ,
running _count : 9 ,
federation _peers : 4 ,
recent _alerts : [ { rule : 'disk_warning' , message : 'Disk usage approaching 50%' , timestamp : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) } ] ,
containers : [
{ id : 'bitcoin' , state : 'running' , version : '27.0' } ,
{ id : 'lnd' , state : 'running' , version : '0.18.0' } ,
{ id : 'electrs' , state : 'running' , version : '0.10.6' } ,
{ id : 'mempool' , state : 'running' , version : '3.0.0' } ,
] ,
reported _at : new Date ( Date . now ( ) - 30000 ) . toISOString ( ) ,
} ,
] ,
} ,
} )
}
case 'telemetry.fleet-alerts' : {
return res . json ( {
result : {
alerts : [
{ node _id : 'archy-198' , rule : 'container_crash' , message : 'electrs restarted 2x in 1h' , timestamp : new Date ( Date . now ( ) - 7200000 ) . toISOString ( ) } ,
{ node _id : 'arch-3' , rule : 'disk_warning' , message : 'Disk usage approaching 50%' , timestamp : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) } ,
{ node _id : 'arch-1' , rule : 'mem_high' , message : 'Memory usage above 60%' , timestamp : new Date ( Date . now ( ) - 86400000 ) . toISOString ( ) } ,
] ,
} ,
} )
}
case 'telemetry.fleet-node-history' : {
const nodeId = params ? . node _id || 'archy-228'
const now = Date . now ( )
const history = Array . from ( { length : 24 } , ( _ , i ) => ( {
timestamp : new Date ( now - ( 23 - i ) * 3600000 ) . toISOString ( ) ,
cpu _pct : + ( 10 + Math . random ( ) * 30 ) . toFixed ( 1 ) ,
mem _pct : + ( 30 + Math . random ( ) * 20 ) . toFixed ( 1 ) ,
disk _pct : + ( 30 + Math . random ( ) * 5 ) . toFixed ( 1 ) ,
} ) )
return res . json ( { result : { history } } )
}
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
case 'analytics.get-snapshot' :
case 'telemetry.report' : {
return res . json ( { result : {
node _id : 'mock-dev-node' ,
version : '1.2.0-alpha' ,
uptime _secs : 86400 ,
cpu _cores : 4 ,
ram _mb : 16384 ,
container _count : 12 ,
running _count : 10 ,
federation _peers : 2 ,
recent _alerts : [ ] ,
reported _at : new Date ( ) . toISOString ( ) ,
} } )
}
2026-03-18 21:06:14 +00:00
// System / Network / Updates
// =====================================================================
case 'system.stats' : {
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
cpu _usage _percent : + ( 12 + Math . random ( ) * 18 ) . toFixed ( 1 ) ,
2026-03-18 21:06:14 +00:00
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 + Math . floor ( Math . random ( ) * 10_000_000_000 ) ,
disk _total _bytes : 1_800_000_000_000 ,
uptime _secs : Math . floor ( process . uptime ( ) ) + 604800 ,
load _avg : [ + ( 0.5 + Math . random ( ) * 1.5 ) . toFixed ( 2 ) , + ( 0.8 + Math . random ( ) ) . toFixed ( 2 ) , + ( 0.6 + Math . random ( ) ) . toFixed ( 2 ) ] ,
net _rx _bytes : 12_400_000_000 ,
net _tx _bytes : 8_900_000_000 ,
} ,
} )
}
case 'update.status' : {
return res . json ( {
result : {
2026-06-22 09:28:05 -04:00
current _version : APP _VERSION ,
latest _version : APP _VERSION ,
update _available : false ,
release _notes : 'You are running the latest demo build.' ,
2026-03-18 21:06:14 +00:00
channel : 'stable' ,
} ,
} )
}
2026-06-17 19:21:42 -04:00
case 'seed.reveal' : {
if ( ! params || ! params . password ) {
return res . json ( { error : { code : - 32000 , message : 'Password is required to reveal the recovery phrase' } } )
}
// Demo gate: accept any non-empty password; reject "wrong" to exercise the error path.
if ( params . password === 'wrong' ) {
return res . json ( { error : { code : - 32000 , message : 'Incorrect password' } } )
}
const demo = 'legal winner thank year wave sausage worth useful legal winner thank yellow able cabin dad debris during dose talent layer crater proud drift movie' . split ( ' ' )
return res . json ( { result : { words : demo , word _count : demo . length } } )
}
case 'update.list-mirrors' : {
globalThis . _ _mockMirrors || = [
2026-07-14 11:38:51 -04:00
// Neutral placeholder — the public demo must not reveal the real release-server address.
{ url : 'https://updates.archipelago.demo/releases/manifest.json' , label : 'Origin' } ,
2026-06-17 19:21:42 -04:00
]
return res . json ( { result : { mirrors : globalThis . _ _mockMirrors } } )
}
case 'update.get-source' : {
globalThis . _ _swarmPrefs || = { source : 'origin' , provide _dht : true }
return res . json ( {
result : {
source : globalThis . _ _swarmPrefs . source ,
provide _dht : globalThis . _ _swarmPrefs . provide _dht ,
swarm _available : false , // default build has no iroh-swarm feature
swarm _enabled : false ,
} ,
} )
}
case 'update.set-source' : {
globalThis . _ _swarmPrefs || = { source : 'origin' , provide _dht : true }
if ( params && ( params . source === 'origin' || params . source === 'swarm' ) ) {
globalThis . _ _swarmPrefs . source = params . source
}
if ( params && typeof params . provide === 'boolean' ) {
globalThis . _ _swarmPrefs . provide _dht = params . provide
}
return res . json ( {
result : {
source : globalThis . _ _swarmPrefs . source ,
provide _dht : globalThis . _ _swarmPrefs . provide _dht ,
swarm _available : false ,
swarm _enabled : false ,
} ,
} )
}
2026-03-18 21:06:14 +00:00
case 'network.list-requests' : {
return res . json ( {
result : {
requests : [
{ id : 'req-1' , from _did : 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9' , from _name : 'archy-198' , type : 'federation-join' , status : 'pending' , created _at : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) } ,
] ,
} ,
} )
}
2026-04-11 13:35:52 +01:00
// ── Monitoring ──────────────────────────────────────────────
case 'monitoring.current' : {
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
return res . json ( { result : monitoringSnapshot ( Math . floor ( Date . now ( ) / 1000 ) ) } )
2026-04-11 13:35:52 +01:00
}
case 'monitoring.history' : {
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
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 } } )
2026-04-11 13:35:52 +01:00
}
case 'monitoring.alerts' : {
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
const nowSecs = Math . floor ( Date . now ( ) / 1000 )
2026-04-11 13:35:52 +01:00
return res . json ( {
result : {
alerts : [
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
{ 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 } ,
2026-04-11 13:35:52 +01:00
] ,
} ,
} )
}
case 'monitoring.alert-rules' : {
return res . json ( {
result : {
rules : [
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
{ 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' } ,
2026-04-11 13:35:52 +01:00
] ,
} ,
} )
}
case 'monitoring.configure-alert' :
case 'monitoring.acknowledge-alert' : {
return res . json ( { result : { success : true } } )
}
case 'monitoring.export' : {
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
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 } } )
2026-04-11 13:35:52 +01:00
}
case 'monitoring.container-metrics' : {
return res . json ( {
result : {
containers : [
{ name : 'bitcoin-knots' , cpu _percent : 8.2 , mem _used _bytes : 1_200_000_000 , net _rx _bytes : 5_000_000 , net _tx _bytes : 3_200_000 } ,
{ name : 'lnd' , cpu _percent : 3.1 , mem _used _bytes : 480_000_000 , net _rx _bytes : 2_100_000 , net _tx _bytes : 1_800_000 } ,
{ name : 'electrs' , cpu _percent : 12.4 , mem _used _bytes : 890_000_000 , net _rx _bytes : 800_000 , net _tx _bytes : 600_000 } ,
{ name : 'mempool' , cpu _percent : 5.6 , mem _used _bytes : 320_000_000 , net _rx _bytes : 1_500_000 , net _tx _bytes : 900_000 } ,
{ name : 'filebrowser' , cpu _percent : 0.8 , mem _used _bytes : 45_000_000 , net _rx _bytes : 200_000 , net _tx _bytes : 150_000 } ,
] ,
} ,
} )
}
// ── Fleet / Telemetry ─────────────────────────────────────
case 'telemetry.fleet-status' : {
return res . json ( {
result : {
nodes : [
{
id : 'node-1' , name : 'archy-main' , did : 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9' ,
status : 'online' , last _seen : new Date ( ) . toISOString ( ) ,
version : '1.3.0' , uptime _secs : 604800 ,
system : { cpu _percent : 15.2 , mem _used _bytes : 6_200_000_000 , mem _total _bytes : 16_000_000_000 , disk _used _bytes : 620_000_000_000 , disk _total _bytes : 1_800_000_000_000 } ,
apps : [ 'bitcoin-knots' , 'lnd' , 'electrs' , 'mempool' , 'filebrowser' ] ,
tor _connected : true ,
} ,
{
id : 'node-2' , name : 'archy-198' , did : 'did:key:z6Mkp2z3PJbJHbQ95fGk3CqYVNEPE3VNnFGA7yUYkQjXoZTL' ,
status : 'online' , last _seen : new Date ( Date . now ( ) - 120000 ) . toISOString ( ) ,
version : '1.2.1' , uptime _secs : 259200 ,
system : { cpu _percent : 8.7 , mem _used _bytes : 3_100_000_000 , mem _total _bytes : 8_000_000_000 , disk _used _bytes : 180_000_000_000 , disk _total _bytes : 500_000_000_000 } ,
apps : [ 'bitcoin-knots' , 'lnd' , 'mempool' ] ,
tor _connected : true ,
} ,
{
id : 'node-3' , name : 'archy-vps' , did : 'did:key:z6MknGc3ocHs3zdPiJbnaaqDi5ER7eLYwBqPR4NkDhCUD7Li' ,
status : 'offline' , last _seen : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) ,
version : '1.3.0' , uptime _secs : 0 ,
system : { cpu _percent : 0 , mem _used _bytes : 0 , mem _total _bytes : 4_000_000_000 , disk _used _bytes : 45_000_000_000 , disk _total _bytes : 80_000_000_000 } ,
apps : [ 'lnd' ] ,
tor _connected : false ,
} ,
] ,
} ,
} )
}
case 'telemetry.fleet-alerts' : {
return res . json ( {
result : {
alerts : [
{ id : 'fa1' , node _id : 'node-3' , node _name : 'archy-vps' , kind : 'node_offline' , message : 'Node went offline' , timestamp : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) , acknowledged : false } ,
{ id : 'fa2' , node _id : 'node-2' , node _name : 'archy-198' , kind : 'disk_high' , message : 'Disk usage at 36%' , timestamp : new Date ( Date . now ( ) - 86400000 ) . toISOString ( ) , acknowledged : true } ,
] ,
} ,
} )
}
case 'telemetry.fleet-node-history' : {
const nodeId = params ? . node _id || 'node-1'
const points = 60
const history = [ ]
for ( let i = 0 ; i < points ; i ++ ) {
history . push ( {
timestamp : new Date ( Date . now ( ) - ( points - i ) * 60000 ) . toISOString ( ) ,
cpu _percent : + ( 8 + Math . random ( ) * 20 ) . toFixed ( 1 ) ,
mem _used _bytes : 5_000_000_000 + Math . floor ( Math . random ( ) * 2_000_000_000 ) ,
} )
}
return res . json ( { result : { node _id : nodeId , history } } )
}
2026-07-14 06:40:26 -04:00
// ── OpenWRT / TollGate gateway (demo: a thoroughly-used gateway) ──
case 'openwrt.get-status' : {
return res . json ( {
result : {
host : '192.168.8.1' ,
hostname : 'archy-tollgate' ,
uptime _secs : 19 * 86400 + 7 * 3600 + 42 * 60 ,
release : {
openwrt _release : 'OpenWrt 24.10.1' ,
openwrt _version : 'r28597-0425664679' ,
openwrt _board _name : 'glinet,gl-mt3000' ,
openwrt _arch : 'aarch64_cortex-a53' ,
openwrt _target : 'mediatek/filogic' ,
} ,
tollgate : {
installed : true ,
enabled : mockState . tollgateEnabled ,
metric : 'milliseconds' ,
step _size _ms : mockState . tollgateStepMs ,
price _per _step : mockState . tollgatePrice ,
min _steps : mockState . tollgateMinSteps ,
currency : 'sat' ,
mint _url : mockState . tollgateMint ,
} ,
wifi _interfaces : [
{ section : 'default_radio0' , ssid : 'TollGate-⚡-2.4G' , device : 'radio0' , encryption : 'none' , network : 'lan' , disabled : false } ,
{ section : 'default_radio1' , ssid : 'TollGate-⚡-5G' , device : 'radio1' , encryption : 'none' , network : 'lan' , disabled : false } ,
{ section : 'wifinet2' , ssid : 'Archy-Private' , device : 'radio1' , encryption : 'sae' , network : 'lan' , disabled : false } ,
] ,
wan : {
configured : true ,
ssid : 'CasaDelSol-5G' ,
assoc _ssid : 'CasaDelSol-5G' ,
encryption : 'psk2' ,
ip : '192.168.1.187' ,
internet : true ,
radio0 _disabled : false ,
sta _iface : 'wifinet1' ,
sta _state : 'COMPLETED' ,
wifi _log : 'wlan1: associated -> COMPLETED (RSSI -52 dBm)' ,
lan _ip : '172.16.0.1' ,
lan _netmask : '255.255.255.0' ,
dhcp _start : '100' ,
dhcp _limit : '150' ,
masq : true ,
} ,
} ,
} )
}
case 'openwrt.scan' : {
return res . json ( { result : { routers : [ '192.168.8.1' ] } } )
}
case 'openwrt.scan-wifi' : {
return res . json ( {
result : {
networks : [
{ ssid : 'CasaDelSol-5G' , bssid : 'a4:3e:51:0b:77:21' , signal : - 52 , channel : 44 , encryption : 'psk2' } ,
{ ssid : 'CasaDelSol' , bssid : 'a4:3e:51:0b:77:20' , signal : - 49 , channel : 6 , encryption : 'psk2' } ,
{ ssid : 'Vodafone-B221' , bssid : '5c:35:3b:9a:12:f0' , signal : - 71 , channel : 1 , encryption : 'psk2' } ,
{ ssid : 'cafe-guest' , bssid : '10:27:f5:c4:88:3a' , signal : - 78 , channel : 11 , encryption : 'none' } ,
] ,
} ,
} )
}
case 'openwrt.provision-tollgate' : {
if ( params ? . price _per _step != null ) mockState . tollgatePrice = params . price _per _step
if ( params ? . step _size _ms != null ) mockState . tollgateStepMs = params . step _size _ms
if ( params ? . min _steps != null ) mockState . tollgateMinSteps = params . min _steps
if ( params ? . mint _url ) mockState . tollgateMint = params . mint _url
if ( params ? . enabled != null ) mockState . tollgateEnabled = ! ! params . enabled
return res . json ( { result : { ok : true } } )
}
case 'openwrt.configure-wan' : {
return res . json ( { result : { ok : true } } )
}
// ── Nostr peer discovery (demo) ──────────────────────────────────
case 'nostr.discovery-status' : {
return res . json ( { result : { enabled : mockState . nostrDiscovery } } )
}
case 'nostr.set-discovery' : {
mockState . nostrDiscovery = ! ! params ? . enabled
return res . json ( { result : { enabled : mockState . nostrDiscovery } } )
}
case 'handshake.discover' : {
return res . json ( {
result : {
nodes : [
{ nostr _pubkey : '8f1c9a2b7d4e6f0a3c5b8d1e4f7a0c3b6e9d2f5a8b1c4e7d0a3f6c9b2e5d8a1f' , nostr _npub : 'npub13uwf52m04893s79hxc7fmz57jd60jz4kd3nk28h5t2rr3l2xkq0qte0mfz' , did : 'did:key:z6MkfV2sQpXm4d8YtR1nWc7uHb3eKj9gLa5xPzD6oTiN8rEw' , version : '1.7.99' } ,
{ nostr _pubkey : '2a4c6e8f0b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f1a3c5e7b9d1f3a' , nostr _npub : 'npub1z5jvknq0k8f7am7wp0946fmdxu2yjkzr8mfw2n0lqv4tksz3xy8s5c9m2d' , did : 'did:key:z6MkrJ8pWx2yNc5vT9qLb4eHu7dKf1gMa3sPzE6oXiQ8nRvw' , version : '1.7.99' } ,
{ nostr _pubkey : '5b7d9f1a3c5e7b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d' , nostr _npub : 'npub1t9k2vfmq7x8n3a5wj0c4bz6y1d8s2e7r5m9p0l3q6u4i8o2h7g1f5x9c3vk' , did : 'did:key:z6MkhT4wQn8xPc2vL6sRb9eYu3dJf7gKa1mNzD5oWiE8tXvq' , version : '1.7.98' } ,
{ nostr _pubkey : '9c1e3b5d7f9a1c3e5b7d9f1a3c5e7b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e' , nostr _npub : 'npub1w3e8rkmq2x7n9a4vj5c0bz1y6d3s8e2r7m4p9l0q5u8i3o6h2g7f1x4c9vs' , did : 'did:key:z6MkgS9wPm3xNc7vK2sQb5eTu8dHf4gJa6mLzD1oXiR8nEvw' , version : '1.7.99' } ,
] ,
} ,
} )
}
case 'handshake.connect' : {
const reqId = ` preq- ${ Date . now ( ) } `
mockState . pendingPeerRequests . push ( {
id : reqId ,
from _nostr _pubkey : params ? . recipient _nostr _pubkey || 'unknown' ,
from _nostr _npub : 'npub1outbounddemo' ,
from _did : 'did:key:zOutboundDemo' ,
from _name : params ? . name || null ,
message : params ? . message || null ,
received _at : new Date ( ) . toISOString ( ) ,
state : 'pending' ,
outbound : true ,
} )
return res . json ( { result : { ok : true , sent _to : params ? . recipient _nostr _pubkey || '' , id : reqId } } )
}
case 'handshake.poll' : {
return res . json ( { result : { polled : 4 , new _requests : [ ] , applied _invites : [ ] , rejected _outbound : [ ] , skipped : [ ] } } )
}
case 'federation.list-pending-requests' : {
return res . json ( { result : { requests : mockState . pendingPeerRequests } } )
}
case 'federation.approve-request' : {
const r = mockState . pendingPeerRequests . find ( ( x ) => x . id === params ? . id )
if ( r ) r . state = 'approved'
return res . json ( { result : { approved : true , id : params ? . id || '' } } )
}
case 'federation.reject-request' : {
const r = mockState . pendingPeerRequests . find ( ( x ) => x . id === params ? . id )
if ( r ) r . state = 'rejected'
return res . json ( { result : { rejected : true , id : params ? . id || '' } } )
}
2026-01-24 22:59:20 +00:00
default : {
2026-03-07 22:36:45 +00:00
console . log ( ` [RPC] Unknown method: ${ method } ` )
2026-01-24 22:59:20 +00:00
return res . json ( {
error : {
code : - 32601 ,
message : ` Method not found: ${ method } ` ,
} ,
} )
}
}
} catch ( error ) {
console . error ( '[RPC Error]' , error )
return res . json ( {
error : {
code : - 32603 ,
message : error . message ,
} ,
} )
}
} )
2026-03-07 22:50:05 +00:00
// =============================================================================
// Mock FileBrowser API (for Cloud page in demo/Docker deployments)
// =============================================================================
2026-06-22 09:28:05 -04:00
const SEED _FILES = {
2026-03-07 22:50:05 +00:00
'/' : [
{ 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 : '' } ,
] ,
'/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' } ,
{ name : 'Architects of Tomorrow.wav' , path : '/Music/Architects of Tomorrow.wav' , size : 42_000_000 , modified : '2025-01-08T15:00:00Z' , isDir : false , type : 'audio' } ,
{ name : 'Sats or Shackles.mp3' , path : '/Music/Sats or Shackles.mp3' , size : 6_200_000 , modified : '2024-12-20T10:00:00Z' , isDir : false , type : 'audio' } ,
{ name : 'The Four Horseman of Technocracy.mp3' , path : '/Music/The Four Horseman of Technocracy.mp3' , size : 7_800_000 , modified : '2024-12-15T11:00:00Z' , isDir : false , type : 'audio' } ,
{ name : 'Inverse Dylan (Remix).mp3' , path : '/Music/Inverse Dylan (Remix).mp3' , size : 5_600_000 , modified : '2024-12-10T16:00:00Z' , isDir : false , type : 'audio' } ,
{ name : 'Hootcoiner.mp3' , path : '/Music/Hootcoiner.mp3' , size : 4_200_000 , modified : '2024-11-28T09:00:00Z' , isDir : false , type : 'audio' } ,
{ name : 'decentrealisation.mp3' , path : '/Music/decentrealisation.mp3' , size : 5_100_000 , modified : '2024-11-20T14:00:00Z' , isDir : false , type : 'audio' } ,
{ name : 'neo-morality.mp3' , path : '/Music/neo-morality.mp3' , size : 6_800_000 , modified : '2024-11-15T11:00:00Z' , isDir : false , type : 'audio' } ,
{ name : 'death is a gift.mp3' , path : '/Music/death is a gift.mp3' , size : 4_500_000 , modified : '2024-11-10T08:00:00Z' , isDir : false , type : 'audio' } ,
{ name : 'Wash the fucking dishes.mp3' , path : '/Music/Wash the fucking dishes.mp3' , size : 3_900_000 , modified : '2024-11-05T13:00:00Z' , isDir : false , type : 'audio' } ,
{ name : 'All the leaves are brown.mp3' , path : '/Music/All the leaves are brown.mp3' , size : 5_300_000 , modified : '2024-10-28T10:00:00Z' , isDir : false , type : 'audio' } ,
{ name : 'Builders not talkers.mp3' , path : '/Music/Builders not talkers.mp3' , size : 4_700_000 , modified : '2024-10-20T15:00:00Z' , isDir : false , type : 'audio' } ,
{ name : 'SMRI.mp3' , path : '/Music/SMRI.mp3' , size : 5_900_000 , modified : '2024-10-15T12:00:00Z' , isDir : false , type : 'audio' } ,
{ name : 'Shadrap.mp3' , path : '/Music/Shadrap.mp3' , size : 3_400_000 , modified : '2024-10-10T09:00:00Z' , isDir : false , type : 'audio' } ,
{ name : 'The Wehrman.mp3' , path : '/Music/The Wehrman.mp3' , size : 6_100_000 , modified : '2024-10-05T14:00:00Z' , isDir : false , type : 'audio' } ,
{ name : 'An Exploited Substrate.mp3' , path : '/Music/An Exploited Substrate.mp3' , size : 4_800_000 , modified : '2024-09-28T11:00:00Z' , isDir : false , type : 'audio' } ,
{ name : 'Govcucks.wav' , path : '/Music/Govcucks.wav' , size : 38_000_000 , modified : '2024-09-20T16:00:00Z' , isDir : false , type : 'audio' } ,
] ,
'/Documents' : [
{ name : 'bitcoin-whitepaper-notes.md' , path : '/Documents/bitcoin-whitepaper-notes.md' , size : 820 , modified : '2025-02-28T14:30:00Z' , isDir : false , type : 'text' } ,
{ name : 'node-setup-checklist.md' , path : '/Documents/node-setup-checklist.md' , size : 950 , modified : '2025-02-25T10:00:00Z' , isDir : false , type : 'text' } ,
{ name : 'lightning-channels.csv' , path : '/Documents/lightning-channels.csv' , size : 680 , modified : '2025-02-20T16:00:00Z' , isDir : false , type : 'text' } ,
{ name : 'sovereignty-manifesto.txt' , path : '/Documents/sovereignty-manifesto.txt' , size : 1100 , modified : '2025-02-15T12:00:00Z' , isDir : false , type : 'text' } ,
{ name : 'backup-log.json' , path : '/Documents/backup-log.json' , size : 1450 , modified : '2025-03-01T02:00:00Z' , isDir : false , type : 'text' } ,
] ,
'/Photos' : [
{ name : 'node-rack-setup.jpg' , path : '/Photos/node-rack-setup.jpg' , size : 2_400_000 , modified : '2025-02-20T09:15:00Z' , isDir : false , type : 'image' } ,
{ name : 'bitcoin-conference-2024.jpg' , path : '/Photos/bitcoin-conference-2024.jpg' , size : 3_100_000 , modified : '2024-12-15T14:30:00Z' , isDir : false , type : 'image' } ,
{ name : 'lightning-network-visualization.png' , path : '/Photos/lightning-network-visualization.png' , size : 1_800_000 , modified : '2025-01-10T11:00:00Z' , isDir : false , type : 'image' } ,
{ 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' } ,
] ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
// 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).
2026-03-07 22:50:05 +00:00
}
2026-06-22 09:28:05 -04:00
const SEED _FILE _CONTENTS = {
2026-03-07 22:50:05 +00:00
'/Documents/bitcoin-whitepaper-notes.md' : ` # Bitcoin Whitepaper Notes \n \n ## Key Concepts \n \n ### Peer-to-Peer Electronic Cash \n - No trusted third party needed \n - Double-spending solved via proof-of-work \n - Longest chain = truth \n \n ### Proof of Work \n - SHA-256 based hashing \n - Difficulty adjusts every 2016 blocks (~2 weeks) \n - Incentive: block reward + transaction fees \n \n ## My Thoughts \n - The 21M supply cap is genius - digital scarcity \n - Lightning Network solves the scaling concern \n - Self-custody is the whole point ` ,
'/Documents/node-setup-checklist.md' : ` # Archipelago Node Setup Checklist \n \n ## Hardware \n - [x] Intel NUC / Mini PC (16GB RAM minimum) \n - [x] 2TB NVMe SSD \n - [x] USB drive for installer \n - [x] Ethernet cable \n \n ## Core Apps \n - [x] Bitcoin Knots \n - [x] LND \n - [x] Mempool Explorer \n - [ ] BTCPay Server \n - [ ] Fedimint ` ,
'/Documents/lightning-channels.csv' : ` channel_id,peer_alias,capacity_sats,local_balance,remote_balance,status \n ch_001,ACINQ,5000000,2450000,2550000,active \n ch_002,WalletOfSatoshi,2000000,1200000,800000,active \n ch_003,Voltage,10000000,4500000,5500000,active \n ch_004,Kraken,3000000,1800000,1200000,active ` ,
'/Documents/sovereignty-manifesto.txt' : ` THE SOVEREIGNTY MANIFESTO \n ========================= \n \n We hold these truths to be self-evident: \n \n 1. Your data belongs to you. \n 2. Your money should be uncensorable. \n 3. Your communications should be private. \n 4. Your compute should be sovereign. \n 5. Your identity should be self-issued. \n \n Run your own node. Hold your own keys. Own your own data. Be sovereign. ` ,
'/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 ) ,
}
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
// Curated real files: drop files into <repo>/demo/files/<Folder>/<file> (or the
// committed library in <repo>/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.
2026-06-22 11:11:40 -04:00
const diskFilePaths = { }
function loadDemoDiskFiles ( ) {
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
// 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' ) ,
]
2026-06-22 11:11:40 -04:00
const tree = { '/' : [ ] }
const contents = { }
const TEXT _EXT = new Set ( [ 'txt' , 'md' , 'json' , 'csv' , 'log' , 'yaml' , 'yml' , 'xml' , 'conf' , 'ini' ] )
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
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
}
}
2026-06-22 11:11:40 -04:00
const walk = ( absDir , relDir ) => {
let entries
try { entries = fsSync . readdirSync ( absDir , { withFileTypes : true } ) } catch { return }
tree [ relDir ] = tree [ relDir ] || [ ]
for ( const e of entries ) {
if ( e . name . startsWith ( '.' ) ) continue
const abs = path . join ( absDir , e . name )
if ( e . isDirectory ( ) ) {
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
const rel = relDir === '/' ? ` / ${ e . name } ` : ` ${ relDir } / ${ e . name } `
2026-06-22 11:11:40 -04:00
tree [ relDir ] . push ( { name : e . name , path : rel , size : 0 , modified : new Date ( ) . toISOString ( ) , isDir : true , type : '' } )
walk ( abs , rel )
} else {
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
addFile ( abs , relDir , e . name )
2026-06-22 11:11:40 -04:00
}
}
}
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
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
2026-06-22 16:45:26 -04:00
// 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.
const provided = new Set ( tree [ '/' ] . map ( e => e . name ) )
for ( const k of Object . keys ( SEED _FILES ) ) {
if ( k !== '/' && provided . has ( k . split ( '/' ) [ 1 ] ) ) delete SEED _FILES [ k ]
}
for ( const k of Object . keys ( SEED _FILE _CONTENTS ) ) {
if ( provided . has ( k . split ( '/' ) [ 1 ] ) ) delete SEED _FILE _CONTENTS [ k ]
}
const keptRoot = ( SEED _FILES [ '/' ] || [ ] ) . filter ( e => ! provided . has ( e . name ) )
for ( const [ k , v ] of Object . entries ( tree ) ) { if ( k !== '/' ) SEED _FILES [ k ] = v }
SEED _FILES [ '/' ] = [ ... keptRoot , ... tree [ '/' ] ]
2026-06-22 11:11:40 -04:00
Object . assign ( SEED _FILE _CONTENTS , contents )
2026-06-22 16:45:26 -04:00
console . log ( ` [Demo] Merged curated files from demo/files ( ${ Object . keys ( diskFilePaths ) . length } binary, ${ Object . keys ( contents ) . length } text; folders: ${ [ ... provided ] . join ( ', ' ) } ) ` )
2026-06-22 11:11:40 -04:00
}
loadDemoDiskFiles ( )
2026-03-09 19:32:28 +00:00
// FileBrowser UI (demo placeholder when launched directly)
app . get ( '/app/filebrowser/' , ( req , res ) => {
res . type ( 'html' ) . send ( ` <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
< title > File Browser < / t i t l e > < s t y l e > * { m a r g i n : 0 ; p a d d i n g : 0 ; b o x - s i z i n g : b o r d e r - b o x } b o d y { b a c k g r o u n d : # 1 a 1 a 2 e ; c o l o r : # e 0 e 0 e 0 ; f o n t - f a m i l y : s y s t e m - u i , s a n s - s e r i f ; d i s p l a y : f l e x ; a l i g n - i t e m s : c e n t e r ; j u s t i f y - c o n t e n t : c e n t e r ; m i n - h e i g h t : 1 0 0 v h }
. card { background : rgba ( 0 , 0 , 0 , 0.4 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ; border - radius : 16 px ; padding : 48 px ; text - align : center ; max - width : 400 px ; backdrop - filter : blur ( 20 px ) }
h1 { font - size : 24 px ; margin - bottom : 12 px } p { color : rgba ( 255 , 255 , 255 , 0.6 ) ; font - size : 14 px ; line - height : 1.6 } < / s t y l e > < / h e a d >
< body > < div class = "card" > < h1 > File Browser < / h 1 > < p > F i l e B r o w s e r i s r u n n i n g . U s e t h e C l o u d p a g e i n A r c h i p e l a g o t o m a n a g e y o u r f i l e s . < / p > < / d i v > < / b o d y > < / h t m l > ` )
} )
2026-03-07 22:50:05 +00:00
// FileBrowser login - return mock JWT
app . post ( '/app/filebrowser/api/login' , ( req , res ) => {
res . send ( '"mock-filebrowser-token-demo"' )
} )
2026-06-22 09:28:05 -04:00
// ── Per-session file store helpers ──────────────────────────────────────────
// store().files = { tree: { '<dir>': [entries] }, contents: { '<path>': string|Buffer }, bytes }
const FB _QUOTA _BYTES = Number ( process . env . DEMO _FILE _QUOTA _BYTES ) || 50 * 1024 * 1024
2026-03-07 22:50:05 +00:00
2026-06-22 09:28:05 -04:00
function fbNormalize ( raw ) {
// → leading slash, no trailing slash (root stays '/')
const p = '/' + decodeURIComponent ( raw || '' ) . split ( '/' ) . filter ( Boolean ) . join ( '/' )
return p === '/' ? '/' : p . replace ( /\/+$/ , '' )
}
function fbParent ( p ) {
const i = p . lastIndexOf ( '/' )
return i <= 0 ? '/' : p . slice ( 0 , i )
}
function fbBase ( p ) { return p . slice ( p . lastIndexOf ( '/' ) + 1 ) }
function fbType ( name ) {
const ext = ( name . includes ( '.' ) ? name . split ( '.' ) . pop ( ) : '' ) . toLowerCase ( )
if ( [ 'mp3' , 'wav' , 'flac' , 'ogg' , 'm4a' , 'aac' ] . includes ( ext ) ) return 'audio'
if ( [ 'jpg' , 'jpeg' , 'png' , 'gif' , 'webp' , 'svg' , 'bmp' ] . includes ( ext ) ) return 'image'
if ( [ 'mp4' , 'webm' , 'mov' , 'mkv' , 'avi' ] . includes ( ext ) ) return 'video'
if ( [ 'txt' , 'md' , 'json' , 'csv' , 'log' , 'yaml' , 'yml' , 'xml' , 'conf' , 'ini' ] . includes ( ext ) ) return 'text'
return ''
}
function fbContentType ( name ) {
const t = fbType ( name )
const ext = ( name . includes ( '.' ) ? name . split ( '.' ) . pop ( ) : '' ) . toLowerCase ( )
if ( t === 'audio' ) return ext === 'wav' ? 'audio/wav' : 'audio/mpeg'
if ( t === 'image' ) return ext === 'png' ? 'image/png' : ext === 'svg' ? 'image/svg+xml' : 'image/jpeg'
if ( t === 'video' ) return 'video/mp4'
return 'text/plain; charset=utf-8'
}
function fbListResponse ( res , items ) {
2026-03-07 22:50:05 +00:00
res . json ( {
items ,
numDirs : items . filter ( i => i . isDir ) . length ,
numFiles : items . filter ( i => ! i . isDir ) . length ,
sorting : { by : 'name' , asc : true } ,
} )
2026-06-22 09:28:05 -04:00
}
// FileBrowser list resources (root: /api/resources or /api/resources/)
app . get ( [ '/app/filebrowser/api/resources' , '/app/filebrowser/api/resources/*' ] , ( req , res ) => {
const dir = fbNormalize ( req . params [ 0 ] || '' )
const items = currentStore ( ) . files . tree [ dir ] || [ ]
fbListResponse ( res , items )
2026-03-07 22:50:05 +00:00
} )
2026-06-22 09:28:05 -04:00
// FileBrowser POST = upload a file OR create a folder (trailing slash ⇒ folder)
2026-03-09 18:12:28 +00:00
app . post ( '/app/filebrowser/api/resources/*' , ( req , res ) => {
2026-06-22 09:28:05 -04:00
const store = currentStore ( )
const { tree , contents } = store . files
const isFolder = ( req . params [ 0 ] || '' ) . endsWith ( '/' )
const full = fbNormalize ( req . params [ 0 ] || '' )
const parent = fbParent ( full )
const name = fbBase ( full )
if ( ! name ) return res . sendStatus ( 400 )
if ( ! tree [ parent ] ) tree [ parent ] = [ ]
if ( isFolder ) {
if ( ! tree [ parent ] . some ( e => e . name === name && e . isDir ) ) {
tree [ parent ] . push ( { name , path : full , size : 0 , modified : new Date ( ) . toISOString ( ) , isDir : true , type : '' } )
if ( ! tree [ full ] ) tree [ full ] = [ ]
}
return res . sendStatus ( 200 )
}
// File upload — collect body with a quota guard.
const chunks = [ ]
let size = 0
let aborted = false
req . on ( 'data' , ( c ) => {
size += c . length
if ( store . files . bytes + size > FB _QUOTA _BYTES ) {
aborted = true
req . destroy ( )
return
}
chunks . push ( c )
} )
req . on ( 'end' , ( ) => {
if ( aborted ) return res . status ( 507 ) . send ( 'Demo storage quota exceeded (50 MB)' )
const buf = Buffer . concat ( chunks )
// Replace existing entry of the same name (override=true).
const existing = tree [ parent ] . find ( e => e . name === name && ! e . isDir )
if ( existing ) store . files . bytes -= existing . size
tree [ parent ] = tree [ parent ] . filter ( e => ! ( e . name === name && ! e . isDir ) )
const type = fbType ( name )
tree [ parent ] . push ( { name , path : full , size : buf . length , modified : new Date ( ) . toISOString ( ) , isDir : false , type } )
contents [ full ] = type === 'text' ? buf . toString ( 'utf-8' ) : buf
store . files . bytes += buf . length
res . sendStatus ( 200 )
} )
req . on ( 'error' , ( ) => { if ( ! res . headersSent ) res . sendStatus ( 400 ) } )
2026-03-09 18:12:28 +00:00
} )
2026-06-22 09:28:05 -04:00
// FileBrowser delete (file or folder + its subtree)
2026-03-09 18:12:28 +00:00
app . delete ( '/app/filebrowser/api/resources/*' , ( req , res ) => {
2026-06-22 09:28:05 -04:00
const store = currentStore ( )
const { tree , contents } = store . files
const full = fbNormalize ( req . params [ 0 ] || '' )
const parent = fbParent ( full )
if ( tree [ parent ] ) {
const entry = tree [ parent ] . find ( e => e . path === full )
if ( entry && ! entry . isDir ) store . files . bytes -= entry . size || 0
tree [ parent ] = tree [ parent ] . filter ( e => e . path !== full )
}
// Recursively drop a directory's children.
if ( tree [ full ] ) {
const stack = [ full ]
while ( stack . length ) {
const d = stack . pop ( )
for ( const e of tree [ d ] || [ ] ) {
if ( e . isDir ) stack . push ( e . path )
else { store . files . bytes -= e . size || 0 ; delete contents [ e . path ] }
}
delete tree [ d ]
}
}
delete contents [ full ]
2026-03-09 18:12:28 +00:00
res . sendStatus ( 200 )
} )
2026-06-22 09:28:05 -04:00
// FileBrowser rename/move (PATCH with { destination })
2026-03-09 18:12:28 +00:00
app . patch ( '/app/filebrowser/api/resources/*' , ( req , res ) => {
2026-06-22 09:28:05 -04:00
const store = currentStore ( )
const { tree , contents } = store . files
const full = fbNormalize ( req . params [ 0 ] || '' )
const dest = fbNormalize ( ( req . body && req . body . destination ) || '' )
if ( ! dest || dest === '/' ) return res . sendStatus ( 400 )
const parent = fbParent ( full )
const entry = ( tree [ parent ] || [ ] ) . find ( e => e . path === full )
if ( ! entry ) return res . sendStatus ( 404 )
const newName = fbBase ( dest )
entry . name = newName
entry . path = dest
entry . modified = new Date ( ) . toISOString ( )
entry . type = entry . isDir ? '' : fbType ( newName )
if ( contents [ full ] !== undefined ) { contents [ dest ] = contents [ full ] ; delete contents [ full ] }
2026-03-09 18:12:28 +00:00
res . sendStatus ( 200 )
} )
2026-06-22 09:28:05 -04:00
// FileBrowser raw file content (text reads, blob/stream fetches)
2026-03-07 22:50:05 +00:00
app . get ( '/app/filebrowser/api/raw/*' , ( req , res ) => {
2026-06-22 09:28:05 -04:00
const full = fbNormalize ( req . params [ 0 ] || '' )
2026-06-22 14:19:38 -04:00
// Curated binary files live on disk and stream directly, with HTTP Range
// support so audio/video can seek and play in the browser.
2026-06-22 11:11:40 -04:00
if ( diskFilePaths [ full ] ) {
2026-06-22 14:19:38 -04:00
const abs = diskFilePaths [ full ]
let stat
try { stat = fsSync . statSync ( abs ) } catch { return res . status ( 404 ) . send ( 'File not found' ) }
2026-06-22 11:11:40 -04:00
res . type ( fbContentType ( fbBase ( full ) ) )
2026-06-22 14:19:38 -04:00
res . setHeader ( 'Accept-Ranges' , 'bytes' )
const range = req . headers . range
if ( range ) {
const m = /bytes=(\d*)-(\d*)/ . exec ( range )
let start = m && m [ 1 ] ? parseInt ( m [ 1 ] , 10 ) : 0
let end = m && m [ 2 ] ? parseInt ( m [ 2 ] , 10 ) : stat . size - 1
if ( isNaN ( start ) || start < 0 ) start = 0
if ( isNaN ( end ) || end >= stat . size ) end = stat . size - 1
if ( start > end ) { res . status ( 416 ) . setHeader ( 'Content-Range' , ` bytes */ ${ stat . size } ` ) ; return res . end ( ) }
res . status ( 206 )
res . setHeader ( 'Content-Range' , ` bytes ${ start } - ${ end } / ${ stat . size } ` )
res . setHeader ( 'Content-Length' , end - start + 1 )
return fsSync . createReadStream ( abs , { start , end } )
. on ( 'error' , ( ) => { if ( ! res . headersSent ) res . status ( 404 ) . end ( ) } ) . pipe ( res )
}
res . setHeader ( 'Content-Length' , stat . size )
return fsSync . createReadStream ( abs )
. on ( 'error' , ( ) => { if ( ! res . headersSent ) res . status ( 404 ) . send ( 'File not found' ) } ) . pipe ( res )
2026-06-22 11:11:40 -04:00
}
2026-06-22 09:28:05 -04:00
const content = currentStore ( ) . files . contents [ full ]
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
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' )
}
2026-06-22 09:28:05 -04:00
res . type ( fbContentType ( fbBase ( full ) ) )
res . send ( Buffer . isBuffer ( content ) ? content : String ( content ) )
2026-03-07 22:50:05 +00:00
} )
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
// 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 ` <svg xmlns="http://www.w3.org/2000/svg" width="800" height="500" viewBox="0 0 800 500">
< rect width = "800" height = "500" fill = "#14161c" / >
< rect x = "24" y = "24" width = "752" height = "452" rx = "16" fill = "none" stroke = "#2a2e3a" stroke - width = "2" / >
< circle cx = "400" cy = "215" r = "58" fill = "none" stroke = "#f7931a" stroke - width = "4" opacity = "0.7" / >
< path d = "M370 245 L400 185 L430 245 Z" fill = "#f7931a" opacity = "0.5" / >
< text x = "400" y = "330" text - anchor = "middle" font - family = "system-ui,sans-serif" font - size = "20" fill = "#8a8f9a" > $ { label } < / t e x t >
< text x = "400" y = "360" text - anchor = "middle" font - family = "system-ui,sans-serif" font - size = "13" fill = "#5b6070" > Archipelago demo sample < / t e x t >
< / s v g > `
}
2026-06-22 10:58:58 -04:00
// 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 ( ) {
const s = currentStore ( )
const md = s . mockData
const w = s . walletState
const apps = Object . entries ( md [ 'package-data' ] || { } )
. map ( ( [ id , a ] ) => ` ${ a . title || id } ( ${ a . state || 'running' } ) ` )
return [
'You are the AI assistant built into this Archipelago node. This is a public DEMO node running on Bitcoin signet (testnet) with simulated data — answer as if everything is real, be concise and helpful, and feel free to discuss Bitcoin, Lightning, self-hosting and the node itself.' ,
'' ,
'CURRENT NODE STATE:' ,
` - Software: Archipelago ${ md [ 'server-info' ] ? . version || 'demo' } , Tor address ${ md [ 'server-info' ] ? . [ 'tor-address' ] || 'n/a' } ` ,
` - Bitcoin: signet, block height 902,418, fully synced (Bitcoin Knots). ` ,
` - Wallet: on-chain ${ w . onchain _sats . toLocaleString ( ) } sats, Lightning ${ w . channel _sats . toLocaleString ( ) } sats, ecash ${ w . ecash _sats . toLocaleString ( ) } sats. ` ,
` - Installed apps ( ${ apps . length } ): ${ apps . join ( ', ' ) } . ` ,
` - Network: FIPS encrypted mesh active with 5 authenticated peers; 12 trusted/federated nodes; Tor hidden services online. ` ,
'- The user can install apps, manage their Lightning/ecash wallet, browse & buy peer files, and chat with the mesh — all from this dashboard.' ,
] . join ( '\n' )
}
2026-03-07 23:07:38 +00:00
// Claude API Proxy (reads ANTHROPIC_API_KEY from environment)
2026-03-09 13:03:53 +00:00
// Uses fetch (Node 22+) for reliable DNS resolution and streaming in Docker/Alpine
2026-03-07 23:07:38 +00:00
// =============================================================================
2026-03-09 13:03:53 +00:00
app . post ( '/aiui/api/claude/*' , async ( req , res ) => {
2026-06-22 14:45:04 -04:00
// DEMO: don't call the real model — return a fixed message (also avoids
// spending the shared API key). Replies in the Anthropic streaming or
// non-streaming shape depending on what the client asked for.
if ( DEMO ) {
const MSG = 'Not available in demo, check out the previous chats to experience AIUI'
if ( req . body && req . body . stream === false ) {
return res . json ( {
id : 'msg_demo' , type : 'message' , role : 'assistant' , model : 'claude-demo' ,
content : [ { type : 'text' , text : MSG } ] ,
stop _reason : 'end_turn' , stop _sequence : null ,
usage : { input _tokens : 1 , output _tokens : 14 } ,
} )
}
res . writeHead ( 200 , { 'Content-Type' : 'text/event-stream' , 'Cache-Control' : 'no-cache' , Connection : 'keep-alive' } )
const send = ( event , data ) => res . write ( ` event: ${ event } \n data: ${ JSON . stringify ( data ) } \n \n ` )
send ( 'message_start' , { type : 'message_start' , message : { id : 'msg_demo' , type : 'message' , role : 'assistant' , model : 'claude-demo' , content : [ ] , stop _reason : null , stop _sequence : null , usage : { input _tokens : 1 , output _tokens : 0 } } } )
send ( 'content_block_start' , { type : 'content_block_start' , index : 0 , content _block : { type : 'text' , text : '' } } )
send ( 'content_block_delta' , { type : 'content_block_delta' , index : 0 , delta : { type : 'text_delta' , text : MSG } } )
send ( 'content_block_stop' , { type : 'content_block_stop' , index : 0 } )
send ( 'message_delta' , { type : 'message_delta' , delta : { stop _reason : 'end_turn' , stop _sequence : null } , usage : { output _tokens : 14 } } )
send ( 'message_stop' , { type : 'message_stop' } )
return res . end ( )
}
2026-03-07 23:07:38 +00:00
const apiKey = process . env . ANTHROPIC _API _KEY
if ( ! apiKey ) {
return res . status ( 500 ) . json ( {
type : 'error' ,
error : { type : 'configuration_error' , message : 'ANTHROPIC_API_KEY not configured on server' }
} )
}
const apiPath = '/' + req . params [ 0 ]
2026-03-07 23:22:30 +00:00
2026-03-08 00:21:38 +00:00
// Clean request body for Anthropic API
2026-03-07 23:22:30 +00:00
const body = req . body
2026-03-08 00:21:38 +00:00
if ( body ) {
if ( ! body . max _tokens ) body . max _tokens = 4096
2026-03-08 00:27:34 +00:00
// Fix model IDs — AIUI may send short names
if ( body . model && ! body . model . includes ( '-2' ) ) {
const modelMap = {
'claude-haiku-4.5' : 'claude-haiku-4-5-20251001' ,
'claude-haiku-4-5' : 'claude-haiku-4-5-20251001' ,
'claude-sonnet-4-5' : 'claude-sonnet-4-5-20250514' ,
'claude-sonnet-4.5' : 'claude-sonnet-4-5-20250514' ,
}
if ( modelMap [ body . model ] ) body . model = modelMap [ body . model ]
}
2026-03-08 00:21:38 +00:00
// Strip AIUI-specific fields that Anthropic API rejects
delete body . webSearch
delete body . webResults
delete body . context
2026-06-22 10:58:58 -04:00
// DEMO: ground the assistant in THIS node's (mock) state so it answers
// questions about the local node, its apps, wallet and Bitcoin like a real
// Archipelago assistant — no /seed needed.
if ( DEMO ) {
const ctx = demoNodeContext ( )
if ( typeof body . system === 'string' ) body . system = ctx + '\n\n' + body . system
else if ( Array . isArray ( body . system ) ) body . system = [ { type : 'text' , text : ctx } , ... body . system ]
else body . system = ctx
}
2026-03-07 23:22:30 +00:00
}
const bodyStr = JSON . stringify ( body )
2026-03-09 13:03:53 +00:00
const url = ` https://api.anthropic.com ${ apiPath } `
console . log ( ` [Claude Proxy] → POST ${ url } ( ${ bodyStr . length } bytes, model: ${ body ? . model || 'unknown' } ) ` )
2026-03-07 23:07:38 +00:00
2026-03-09 13:03:53 +00:00
try {
const controller = new AbortController ( )
const timeout = setTimeout ( ( ) => controller . abort ( ) , 60000 )
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 ,
} )
2026-03-07 23:07:38 +00:00
2026-03-09 13:03:53 +00:00
clearTimeout ( timeout )
console . log ( ` [Claude Proxy] ← ${ apiRes . status } ` )
2026-03-07 23:07:38 +00:00
2026-03-09 13:03:53 +00:00
// 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 } ` )
2026-03-07 23:07:38 +00:00
if ( ! res . headersSent ) {
res . status ( 502 ) . json ( {
type : 'error' ,
2026-03-07 23:58:08 +00:00
error : { type : 'proxy_error' , message : msg }
2026-03-07 23:07:38 +00:00
} )
}
2026-03-09 13:03:53 +00:00
}
2026-03-07 23:07:38 +00:00
} )
2026-03-08 01:48:23 +00:00
// Ollama (local AI) proxy — forwards to localhost:11434
app . all ( '/aiui/api/ollama/*' , ( req , res ) => {
const ollamaPath = '/' + req . params [ 0 ]
const bodyStr = JSON . stringify ( req . body )
const options = {
hostname : '127.0.0.1' ,
port : 11434 ,
path : ollamaPath ,
method : req . method ,
headers : {
'Content-Type' : 'application/json' ,
'Content-Length' : Buffer . byteLength ( bodyStr ) ,
} ,
}
const proxyReq = http . request ( options , ( proxyRes ) => {
res . writeHead ( proxyRes . statusCode , proxyRes . headers )
proxyRes . pipe ( res )
} )
proxyReq . on ( 'error' , ( err ) => {
const msg = err . message || err . code || 'Ollama not available'
console . error ( '[Ollama Proxy] Error:' , msg )
if ( ! res . headersSent ) {
res . status ( 502 ) . json ( { error : msg } )
}
} )
if ( req . method !== 'GET' && req . method !== 'HEAD' ) {
proxyReq . write ( bodyStr )
}
proxyReq . end ( )
} )
// =============================================================================
// Ollama Local AI Proxy (forwards to Ollama on localhost:11434)
// =============================================================================
app . all ( '/api/ollama/*' , ( req , res ) => {
const ollamaPath = '/' + req . params [ 0 ]
const isPost = req . method === 'POST'
const bodyStr = isPost ? JSON . stringify ( req . body ) : null
const options = {
hostname : '127.0.0.1' ,
port : 11434 ,
path : ollamaPath ,
method : req . method ,
headers : { 'Content-Type' : 'application/json' } ,
}
const proxyReq = http . request ( options , ( proxyRes ) => {
res . writeHead ( proxyRes . statusCode , proxyRes . headers )
proxyRes . pipe ( res )
} )
proxyReq . on ( 'error' , ( err ) => {
const msg = err . message || err . code || 'Ollama not available'
console . error ( '[Ollama Proxy] Error:' , msg )
if ( ! res . headersSent ) {
res . status ( 502 ) . json ( { error : msg } )
}
} )
if ( bodyStr ) proxyReq . write ( bodyStr )
proxyReq . end ( )
} )
2026-03-07 23:24:27 +00:00
// Web search stub (no search engine configured in demo)
app . get ( '/api/web-search' , ( req , res ) => {
2026-03-07 23:58:08 +00:00
res . json ( { results : [ ] } )
} )
// TMDB API stub (no TMDB key in demo)
app . get ( '/api/tmdb/*' , ( req , res ) => {
res . json ( { results : [ ] } )
2026-03-07 23:24:27 +00:00
} )
2026-03-07 23:58:08 +00:00
// Catch-all for unimplemented API endpoints (return JSON, not HTML)
app . all ( '/api/*' , ( req , res ) => {
res . status ( 404 ) . json ( { error : 'Not available in demo mode' } )
} )
2026-03-07 23:24:27 +00:00
app . all ( '/aiui/api/*' , ( req , res ) => {
2026-03-07 23:58:08 +00:00
res . status ( 404 ) . json ( { error : 'Not available in demo mode' } )
} )
2026-03-18 21:06:14 +00:00
// =============================================================================
// Mock ThunderHub UI + Lightning API (no Docker required)
// =============================================================================
const MOCK _LND _DATA = {
info : {
alias : 'archy-signet' ,
color : '#f7931a' ,
public _key : '03a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
num _active _channels : 4 ,
num _inactive _channels : 1 ,
num _pending _channels : 1 ,
block _height : 892451 ,
synced _to _chain : true ,
synced _to _graph : true ,
version : '0.17.4-beta commit=v0.17.4-beta' ,
chains : [ { chain : 'bitcoin' , network : 'signet' } ] ,
uris : [ '03a1b2c3@archy-signet.onion:9735' ] ,
} ,
balance : {
total _balance : 2_450_000 ,
confirmed _balance : 2_350_000 ,
unconfirmed _balance : 100_000 ,
} ,
channelBalance : {
local _balance : { sat : 8_250_000 } ,
remote _balance : { sat : 11_750_000 } ,
pending _open _local _balance : { sat : 500_000 } ,
pending _open _remote _balance : { sat : 0 } ,
} ,
channels : [
{ chan _id : '840921088114688' , remote _pubkey : '02778f4a4e...acinq' , capacity : 5_000_000 , local _balance : 2_450_000 , remote _balance : 2_550_000 , active : true , peer _alias : 'ACINQ Signet' , total _satoshis _sent : 850_000 , total _satoshis _received : 1_200_000 , uptime : 604800 , lifetime : 2592000 } ,
{ chan _id : '840921088114689' , remote _pubkey : '03abcdef12...wos' , capacity : 2_000_000 , local _balance : 1_200_000 , remote _balance : 800_000 , active : true , peer _alias : 'WalletOfSatoshi' , total _satoshis _sent : 350_000 , total _satoshis _received : 500_000 , uptime : 259200 , lifetime : 1296000 } ,
{ chan _id : '840921088114690' , remote _pubkey : '02fedcba98...voltage' , capacity : 10_000_000 , local _balance : 4_500_000 , remote _balance : 5_500_000 , active : true , peer _alias : 'Voltage' , total _satoshis _sent : 2_100_000 , total _satoshis _received : 1_800_000 , uptime : 518400 , lifetime : 2592000 } ,
{ chan _id : '840921088114691' , remote _pubkey : '03456789ab...kraken' , capacity : 3_000_000 , local _balance : 100_000 , remote _balance : 2_900_000 , active : true , peer _alias : 'Kraken' , total _satoshis _sent : 50_000 , total _satoshis _received : 120_000 , uptime : 86400 , lifetime : 604800 } ,
{ chan _id : '840921088114692' , remote _pubkey : '02112233aa...old' , capacity : 1_000_000 , local _balance : 0 , remote _balance : 1_000_000 , active : false , peer _alias : 'OldPeer-Offline' , total _satoshis _sent : 0 , total _satoshis _received : 0 , uptime : 0 , lifetime : 5184000 } ,
] ,
pendingChannels : {
pending _open _channels : [
{ channel : { remote _node _pub : '03ffeeddcc...newpeer' , capacity : 500_000 , local _balance : 500_000 , remote _balance : 0 } , confirmation _height : 892452 } ,
] ,
pending _closing _channels : [ ] ,
pending _force _closing _channels : [ ] ,
waiting _close _channels : [ ] ,
} ,
invoices : [
{ memo : 'Channel opening fee' , value : 50_000 , settled : true , creation _date : Math . floor ( Date . now ( ) / 1000 ) - 86400 , settle _date : Math . floor ( Date . now ( ) / 1000 ) - 85800 , payment _request : 'lnsb500000n1pjtest...truncated' , state : 'SETTLED' , amt _paid _sat : 50_000 , r _hash : Buffer . from ( 'aabbccdd01' ) . toString ( 'hex' ) } ,
{ memo : 'Test payment' , value : 1_000 , settled : true , creation _date : Math . floor ( Date . now ( ) / 1000 ) - 7200 , settle _date : Math . floor ( Date . now ( ) / 1000 ) - 7100 , payment _request : 'lnsb10000n1pjtest2...truncated' , state : 'SETTLED' , amt _paid _sat : 1_000 , r _hash : Buffer . from ( 'aabbccdd02' ) . toString ( 'hex' ) } ,
{ memo : 'Coffee payment' , value : 5_000 , settled : true , creation _date : Math . floor ( Date . now ( ) / 1000 ) - 3600 , settle _date : Math . floor ( Date . now ( ) / 1000 ) - 3500 , payment _request : 'lnsb50000n1pjtest3...truncated' , state : 'SETTLED' , amt _paid _sat : 5_000 , r _hash : Buffer . from ( 'aabbccdd03' ) . toString ( 'hex' ) } ,
{ memo : 'Donation' , value : 21_000 , settled : false , creation _date : Math . floor ( Date . now ( ) / 1000 ) - 600 , settle _date : 0 , payment _request : 'lnsb210000n1pjtest4...truncated' , state : 'OPEN' , amt _paid _sat : 0 , r _hash : Buffer . from ( 'aabbccdd04' ) . toString ( 'hex' ) } ,
] ,
payments : [
{ payment _hash : 'ff00112233' , value _sat : 10_000 , fee _sat : 3 , status : 'SUCCEEDED' , creation _date : Math . floor ( Date . now ( ) / 1000 ) - 43200 , payment _request : 'lnsb100000n1pjpay1...' , failure _reason : 'FAILURE_REASON_NONE' } ,
{ payment _hash : 'ff00112234' , value _sat : 100_000 , fee _sat : 12 , status : 'SUCCEEDED' , creation _date : Math . floor ( Date . now ( ) / 1000 ) - 21600 , payment _request : 'lnsb1000000n1pjpay2...' , failure _reason : 'FAILURE_REASON_NONE' } ,
{ payment _hash : 'ff00112235' , value _sat : 500 , fee _sat : 1 , status : 'SUCCEEDED' , creation _date : Math . floor ( Date . now ( ) / 1000 ) - 1800 , payment _request : 'lnsb5000n1pjpay3...' , failure _reason : 'FAILURE_REASON_NONE' } ,
] ,
forwarding : [
{ chan _id _in : '840921088114688' , chan _id _out : '840921088114690' , amt _in : 10_012 , amt _out : 10_000 , fee : 12 , timestamp _ns : ( Date . now ( ) - 7200000 ) * 1e6 } ,
{ chan _id _in : '840921088114690' , chan _id _out : '840921088114689' , amt _in : 5_005 , amt _out : 5_000 , fee : 5 , timestamp _ns : ( Date . now ( ) - 3600000 ) * 1e6 } ,
{ chan _id _in : '840921088114689' , chan _id _out : '840921088114691' , amt _in : 25_025 , amt _out : 25_000 , fee : 25 , timestamp _ns : ( Date . now ( ) - 1200000 ) * 1e6 } ,
] ,
}
// ThunderHub mock web UI
app . get ( '/app/thunderhub/' , ( req , res ) => {
const d = MOCK _LND _DATA
const totalCap = d . channels . reduce ( ( s , c ) => s + c . capacity , 0 )
const totalLocal = d . channels . reduce ( ( s , c ) => s + c . local _balance , 0 )
const totalRemote = d . channels . reduce ( ( s , c ) => s + c . remote _balance , 0 )
const totalFees = d . forwarding . reduce ( ( s , f ) => s + f . fee , 0 )
const channelRows = d . channels . map ( c => `
< tr >
< td > $ { c . peer _alias } < / t d >
< td > $ { ( c . capacity / 1e6 ) . toFixed ( 1 ) } M < / t d >
< td > < div style = "display:flex;gap:4px;align-items:center" > < div style = "background:#4ade80;height:8px;width:${Math.round(c.local_balance/c.capacity*100)}%;border-radius:4px" > < / d i v > < s p a n s t y l e = " f o n t - s i z e : 1 1 p x ; o p a c i t y : . 6 " > $ { ( c . l o c a l _ b a l a n c e / 1 e 3 ) . t o F i x e d ( 0 ) } k < / s p a n > < / d i v > < / t d >
< td > < div style = "display:flex;gap:4px;align-items:center" > < div style = "background:#3b82f6;height:8px;width:${Math.round(c.remote_balance/c.capacity*100)}%;border-radius:4px" > < / d i v > < s p a n s t y l e = " f o n t - s i z e : 1 1 p x ; o p a c i t y : . 6 " > $ { ( c . r e m o t e _ b a l a n c e / 1 e 3 ) . t o F i x e d ( 0 ) } k < / s p a n > < / d i v > < / t d >
< td > < span style = "color:${c.active ? '#4ade80' : '#ef4444'}" > $ { c . active ? 'Active' : 'Offline' } < / s p a n > < / t d >
< / t r > ` ) . j o i n ( ' ' )
const invoiceRows = d . invoices . slice ( ) . reverse ( ) . map ( inv => `
< tr >
< td > $ { inv . memo } < / t d >
< td > $ { inv . value . toLocaleString ( ) } sats < / t d >
< td > < span style = "color:${inv.settled ? '#4ade80' : '#fb923c'}" > $ { inv . settled ? 'Settled' : 'Open' } < / s p a n > < / t d >
< td > $ { new Date ( inv . creation _date * 1000 ) . toLocaleString ( ) } < / t d >
< / t r > ` ) . j o i n ( ' ' )
const paymentRows = d . payments . map ( p => `
< tr >
< td > $ { p . payment _hash . slice ( 0 , 12 ) } ... < / t d >
< td > $ { p . value _sat . toLocaleString ( ) } sats < / t d >
< td > $ { p . fee _sat } sats < / t d >
< td style = "color:#4ade80" > Succeeded < / t d >
< / t r > ` ) . j o i n ( ' ' )
res . type ( 'html' ) . send ( ` <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
< title > ThunderHub — archy - signet < / t i t l e >
< style >
* { margin : 0 ; padding : 0 ; box - sizing : border - box }
body { background : # 0 f0f1a ; color : # e0e0e0 ; font - family : system - ui , - apple - system , sans - serif ; padding : 24 px }
h1 { font - size : 22 px ; margin - bottom : 4 px ; color : # fb923c }
. subtitle { color : rgba ( 255 , 255 , 255 , . 5 ) ; font - size : 13 px ; margin - bottom : 24 px }
. grid { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 180 px , 1 fr ) ) ; gap : 16 px ; margin - bottom : 32 px }
. stat { background : rgba ( 255 , 255 , 255 , . 05 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , . 1 ) ; border - radius : 12 px ; padding : 16 px }
. stat . label { font - size : 11 px ; text - transform : uppercase ; letter - spacing : 1 px ; color : rgba ( 255 , 255 , 255 , . 4 ) ; margin - bottom : 4 px }
. stat . value { font - size : 24 px ; font - weight : 600 }
. stat . value . green { color : # 4 ade80 } . stat . value . orange { color : # fb923c } . stat . value . blue { color : # 3 b82f6 }
h2 { font - size : 16 px ; margin : 24 px 0 12 px ; color : rgba ( 255 , 255 , 255 , . 8 ) }
table { width : 100 % ; border - collapse : collapse ; font - size : 13 px }
th { text - align : left ; padding : 8 px 12 px ; border - bottom : 1 px solid rgba ( 255 , 255 , 255 , . 1 ) ; color : rgba ( 255 , 255 , 255 , . 4 ) ; font - weight : 500 ; font - size : 11 px ; text - transform : uppercase ; letter - spacing : . 5 px }
td { padding : 8 px 12 px ; border - bottom : 1 px solid rgba ( 255 , 255 , 255 , . 05 ) }
tr : hover td { background : rgba ( 255 , 255 , 255 , . 02 ) }
. section { background : rgba ( 0 , 0 , 0 , . 3 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , . 08 ) ; border - radius : 12 px ; padding : 20 px ; margin - bottom : 20 px }
. badge { display : inline - block ; background : rgba ( 247 , 147 , 26 , . 15 ) ; color : # fb923c ; padding : 2 px 8 px ; border - radius : 6 px ; font - size : 11 px ; margin - left : 8 px }
< / s t y l e > < / h e a d > < b o d y >
< h1 > ThunderHub < span class = "badge" > signet < / s p a n > < / h 1 >
< div class = "subtitle" > $ { d . info . alias } & mdash ; block $ { d . info . block _height . toLocaleString ( ) } & mdash ; $ { d . info . num _active _channels } active channels < / d i v >
< div class = "grid" >
< div class = "stat" > < div class = "label" > On - chain Balance < / d i v > < d i v c l a s s = " v a l u e o r a n g e " > $ { ( d . b a l a n c e . c o n f i r m e d _ b a l a n c e / 1 e 6 ) . t o F i x e d ( 2 ) } M s a t s < / d i v > < / d i v >
< div class = "stat" > < div class = "label" > Channel Capacity < / d i v > < d i v c l a s s = " v a l u e " > $ { ( t o t a l C a p / 1 e 6 ) . t o F i x e d ( 1 ) } M s a t s < / d i v > < / d i v >
< div class = "stat" > < div class = "label" > Local Balance < / d i v > < d i v c l a s s = " v a l u e g r e e n " > $ { ( t o t a l L o c a l / 1 e 6 ) . t o F i x e d ( 1 ) } M s a t s < / d i v > < / d i v >
< div class = "stat" > < div class = "label" > Remote Balance < / d i v > < d i v c l a s s = " v a l u e b l u e " > $ { ( t o t a l R e m o t e / 1 e 6 ) . t o F i x e d ( 1 ) } M s a t s < / d i v > < / d i v >
< div class = "stat" > < div class = "label" > Routing Fees Earned < / d i v > < d i v c l a s s = " v a l u e g r e e n " > $ { t o t a l F e e s } s a t s < / d i v > < / d i v >
< div class = "stat" > < div class = "label" > Payments Sent < / d i v > < d i v c l a s s = " v a l u e " > $ { d . p a y m e n t s . l e n g t h } < / d i v > < / d i v >
< / d i v >
< div class = "section" >
< h2 > Channels ( $ { d . channels . length } ) < / h 2 >
< table > < thead > < tr > < th > Peer < / t h > < t h > C a p a c i t y < / t h > < t h > L o c a l < / t h > < t h > R e m o t e < / t h > < t h > S t a t u s < / t h > < / t r > < / t h e a d > < t b o d y > $ { c h a n n e l R o w s } < / t b o d y > < / t a b l e >
< / d i v >
< div class = "section" >
< h2 > Recent Invoices < / h 2 >
< table > < thead > < tr > < th > Memo < / t h > < t h > A m o u n t < / t h > < t h > S t a t u s < / t h > < t h > C r e a t e d < / t h > < / t r > < / t h e a d > < t b o d y > $ { i n v o i c e R o w s } < / t b o d y > < / t a b l e >
< / d i v >
< div class = "section" >
< h2 > Recent Payments < / h 2 >
< table > < thead > < tr > < th > Hash < / t h > < t h > A m o u n t < / t h > < t h > F e e < / t h > < t h > S t a t u s < / t h > < / t r > < / t h e a d > < t b o d y > $ { p a y m e n t R o w s } < / t b o d y > < / t a b l e >
< / d i v >
< div class = "section" >
< h2 > Forwarding History < / h 2 >
< table > < thead > < tr > < th > In Channel < / t h > < t h > O u t C h a n n e l < / t h > < t h > A m o u n t < / t h > < t h > F e e < / t h > < t h > T i m e < / t h > < / t r > < / t h e a d > < t b o d y >
$ { d . forwarding . map ( f => {
const inPeer = d . channels . find ( c => c . chan _id === f . chan _id _in ) ? . peer _alias || f . chan _id _in
const outPeer = d . channels . find ( c => c . chan _id === f . chan _id _out ) ? . peer _alias || f . chan _id _out
return ` <tr><td> ${ inPeer } </td><td> ${ outPeer } </td><td> ${ f . amt _in . toLocaleString ( ) } sats</td><td> ${ f . fee } sats</td><td> ${ new Date ( f . timestamp _ns / 1e6 ) . toLocaleString ( ) } </td></tr> `
} ) . join ( '' ) }
< / t b o d y > < / t a b l e >
< / d i v >
< p style = "text-align:center;color:rgba(255,255,255,.25);font-size:12px;margin-top:32px" > Mock ThunderHub & mdash ; Archipelago Dev Mode & mdash ; No Docker Required < / p >
< / b o d y > < / h t m l > ` )
} )
// ThunderHub API stubs (for any programmatic access)
app . get ( '/app/thunderhub/api/info' , ( req , res ) => res . json ( MOCK _LND _DATA . info ) )
app . get ( '/app/thunderhub/api/balance' , ( req , res ) => res . json ( MOCK _LND _DATA . balance ) )
app . get ( '/app/thunderhub/api/channels' , ( req , res ) => res . json ( MOCK _LND _DATA . channels ) )
app . get ( '/app/thunderhub/api/invoices' , ( req , res ) => res . json ( MOCK _LND _DATA . invoices ) )
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 ) )
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
// ── 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 = {
2026-07-15 09:25:38 +01:00
// Placeholder LND dashboard — the real lnd-ui shell reads poorly inside the
// demo iframe. Numbers stay consistent with the /proxy/lnd/v1/* mocks.
lnd : ( ) => demoAppShell ( 'Lightning Network Daemon' , 'archipelago-lnd · v0.18.3-beta · signet' , '/assets/img/app-icons/lnd.png' , `
< div class = "grid" >
< div class = "card" > < div class = "k" > Status < / d i v > < d i v c l a s s = " v " > < s p a n c l a s s = " b a d g e " > R u n n i n g · s y n c e d < / s p a n > < / d i v > < / d i v >
< div class = "card" > < div class = "k" > On - chain < / d i v > < d i v c l a s s = " v " > 2 , 4 5 0 , 0 0 0 s a t s < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Lightning ( local ) < / d i v > < d i v c l a s s = " v " > 8 , 2 5 0 , 0 0 0 s a t s < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Inbound capacity < / d i v > < d i v c l a s s = " v " > 1 1 , 7 5 0 , 0 0 0 s a t s < / d i v > < / d i v >
< / d i v >
< div class = "grid" >
< div class = "card" > < div class = "k" > Channels < / d i v > < d i v c l a s s = " v " > 4 a c t i v e < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Peers < / d i v > < d i v c l a s s = " v " > 1 1 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Block height < / d i v > < d i v c l a s s = " v " > 9 0 2 , 4 1 8 < / d i v > < / d i v >
< / d i v >
< div class = "card" >
< div class = "k" style = "margin-bottom:6px" > Channels < / d i v >
< table >
< tr > < th > Peer < / t h > < t h > C a p a c i t y < / t h > < t h > L o c a l / R e m o t e < / t h > < t h s t y l e = " w i d t h : 3 0 % " > B a l a n c e < / t h > < / t r >
< tr > < td > ACINQ < / t d > < t d > 5 , 0 0 0 , 0 0 0 < / t d > < t d > 2 , 4 5 0 , 0 0 0 / 2 , 5 5 0 , 0 0 0 < / t d > < t d > < d i v c l a s s = " b a r " > < i s t y l e = " w i d t h : 4 9 % " > < / i > < / d i v > < / t d > < / t r >
< tr > < td > Voltage < / t d > < t d > 1 0 , 0 0 0 , 0 0 0 < / t d > < t d > 4 , 5 0 0 , 0 0 0 / 5 , 5 0 0 , 0 0 0 < / t d > < t d > < d i v c l a s s = " b a r " > < i s t y l e = " w i d t h : 4 5 % " > < / i > < / d i v > < / t d > < / t r >
< tr > < td > Kraken < / t d > < t d > 3 , 0 0 0 , 0 0 0 < / t d > < t d > 1 , 8 0 0 , 0 0 0 / 1 , 2 0 0 , 0 0 0 < / t d > < t d > < d i v c l a s s = " b a r " > < i s t y l e = " w i d t h : 6 0 % " > < / i > < / d i v > < / t d > < / t r >
< tr > < td > Wallet of Satoshi < / t d > < t d > 2 , 0 0 0 , 0 0 0 < / t d > < t d > 1 , 2 0 0 , 0 0 0 / 8 0 0 , 0 0 0 < / t d > < t d > < d i v c l a s s = " b a r " > < i s t y l e = " w i d t h : 6 0 % " > < / i > < / d i v > < / t d > < / t r >
< / t a b l e >
< / d i v >
< div class = "card" style = "margin-top:14px" >
< div class = "k" style = "margin-bottom:6px" > Node URI < / d i v >
< div class = "v mono" > 02 c9f0a1 … e47b @ lnd7f3a2c9d1b4e8f6 . onion : 9735 < / d i v >
< / d i v > ` ) ,
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
'btcpay-server' : ( ) => demoAppShell ( 'BTCPay Server' , 'Self-hosted payment processor · signet' , '/assets/img/app-icons/btcpay-server.png' , `
< div class = "grid" >
< div class = "card" > < div class = "k" > Store < / d i v > < d i v c l a s s = " v " > A r c h i p e l a g o S h o p < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Invoices ( 30 d ) < / d i v > < d i v c l a s s = " v " > 4 7 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Settled < / d i v > < d i v c l a s s = " v " > 1 , 2 8 4 , 5 0 0 s a t s < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Lightning < / d i v > < d i v c l a s s = " v " > < s p a n c l a s s = " b a d g e " > C o n n e c t e d < / s p a n > < / d i v > < / d i v >
< / d i v >
< div class = "card" > < table >
< tr > < th > Invoice < / t h > < t h > A m o u n t < / t h > < t h > S t a t u s < / t h > < t h > C r e a t e d < / t h > < / t r >
< tr > < td > INV - 0047 · Sticker pack < / t d > < t d > 2 1 , 0 0 0 s a t s < / t d > < t d > S e t t l e d < / t d > < t d > 2 h a g o < / t d > < / t r >
< tr > < td > INV - 0046 · Node consult < / t d > < t d > 2 5 0 , 0 0 0 s a t s < / t d > < t d > S e t t l e d < / t d > < t d > 9 h a g o < / t d > < / t r >
< tr > < td > INV - 0045 · Coffee < / t d > < t d > 5 , 5 0 0 s a t s < / t d > < t d > S e t t l e d < / t d > < t d > 1 d a g o < / t d > < / t r >
< tr > < td > INV - 0044 · Merch bundle < / t d > < t d > 8 4 , 0 0 0 s a t s < / t d > < t d > E x p i r e d < / t d > < t d > 2 d a g o < / t d > < / t r >
< / t a b l e > < / d i v > ` ) ,
grafana : ( ) => demoAppShell ( 'Grafana' , 'Dashboards · node metrics' , '/assets/img/app-icons/grafana.png' , `
< div class = "grid" >
< div class = "card" > < div class = "k" > Dashboards < / d i v > < d i v c l a s s = " v " > 6 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Data sources < / d i v > < d i v c l a s s = " v " > 3 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Alerts firing < / d i v > < d i v c l a s s = " v " > 0 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Uptime < / d i v > < d i v c l a s s = " v " > 9 9 . 9 8 % < / d i v > < / d i v >
< / d i v >
< div class = "card" > < table >
< tr > < th > Dashboard < / t h > < t h > P a n e l s < / t h > < t h > L a s t v i e w e d < / t h > < / t r >
< tr > < td > Bitcoin Node Overview < / t d > < t d > 1 4 < / t d > < t d > 1 2 m a g o < / t d > < / t r >
< tr > < td > Lightning Routing < / t d > < t d > 9 < / t d > < t d > 1 h a g o < / t d > < / t r >
< tr > < td > System Resources < / t d > < t d > 1 1 < / t d > < t d > 3 h a g o < / t d > < / t r >
< tr > < td > Tor & amp ; Network < / t d > < t d > 7 < / t d > < t d > 1 d a g o < / t d > < / t r >
< / t a b l e > < / d i v > ` ) ,
nextcloud : ( ) => demoAppShell ( 'Nextcloud' , 'Files · Calendar · Contacts' , '/assets/img/app-icons/nextcloud.webp' , `
< div class = "grid" >
< div class = "card" > < div class = "k" > Storage used < /div><div class="v">182 GB / 1.8 TB < /div><div class="bar"><i style="width:10%"></i > < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Files < / d i v > < d i v c l a s s = " v " > 1 2 , 8 4 7 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Shares < / d i v > < d i v c l a s s = " v " > 2 3 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Users < / d i v > < d i v c l a s s = " v " > 3 < / d i v > < / d i v >
< / d i v >
< div class = "card" > < table >
< tr > < th > Recent file < / t h > < t h > S i z e < / t h > < t h > M o d i f i e d < / t h > < / t r >
< tr > < td > node - backup - 2026 - 07. tar . gz < / t d > < t d > 2 . 3 G B < / t d > < t d > T o d a y < / t d > < / t r >
< tr > < td > Sovereign Computing . pdf < / t d > < t d > 4 . 8 M B < / t d > < t d > Y e s t e r d a y < / t d > < / t r >
< tr > < td > family - photos - june / < / t d > < t d > 1 . 2 G B < / t d > < t d > 3 d a g o < / t d > < / t r >
< / t a b l e > < / d i v > ` ) ,
jellyfin : ( ) => demoAppShell ( 'Jellyfin' , 'The Free Software Media System' , '/assets/img/app-icons/jellyfin.webp' , `
< div class = "grid" >
< div class = "card" > < div class = "k" > Movies < / d i v > < d i v c l a s s = " v " > 2 1 4 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Shows < / d i v > < d i v c l a s s = " v " > 3 8 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Music albums < / d i v > < d i v c l a s s = " v " > 1 5 6 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Active streams < / d i v > < d i v c l a s s = " v " > 1 < / d i v > < / d i v >
< / d i v >
< div class = "card" > < table >
< tr > < th > Recently added < / t h > < t h > T y p e < / t h > < t h > A d d e d < / t h > < / t r >
< tr > < td > The Bitcoin Standard ( audiobook ) < / t d > < t d > A u d i o < / t d > < t d > T o d a y < / t d > < / t r >
< tr > < td > Lightning Payment Demo < / t d > < t d > V i d e o < / t d > < t d > 2 d a g o < / t d > < / t r >
< tr > < td > Node Unboxing Timelapse < / t d > < t d > V i d e o < / t d > < t d > 5 d a g o < / t d > < / t r >
< / t a b l e > < / d i v > ` ) ,
vaultwarden : ( ) => demoAppShell ( 'Vaultwarden' , 'Bitwarden-compatible password manager' , '/assets/img/app-icons/vaultwarden.webp' , `
< div class = "grid" >
< div class = "card" > < div class = "k" > Vault items < / d i v > < d i v c l a s s = " v " > 1 8 4 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Logins < / d i v > < d i v c l a s s = " v " > 1 4 2 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Secure notes < / d i v > < d i v c l a s s = " v " > 2 8 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Status < / d i v > < d i v c l a s s = " v " > < s p a n c l a s s = " b a d g e " > V a u l t l o c k e d < / s p a n > < / d i v > < / d i v >
< / d i v >
< div class = "card" >
< div class = "k" style = "margin-bottom:10px" > Security report < / d i v >
< table >
< tr > < td > Weak passwords < / t d > < t d > 3 < / t d > < / t r >
< tr > < td > Reused passwords < / t d > < t d > 1 < / t d > < / t r >
< tr > < td > Exposed in breaches < / t d > < t d > 0 < / t d > < / t r >
< tr > < td > Two - factor enabled < / t d > < t d > Y e s < / t d > < / t r >
< / t a b l e >
< / d i v > ` ) ,
'nostr-rs-relay' : ( ) => demoAppShell ( 'Nostr Relay' , 'wss://relay.archipelago.lan' , '/assets/img/app-icons/nostrudel.svg' , `
< div class = "grid" >
< div class = "card" > < div class = "k" > Events stored < / d i v > < d i v c l a s s = " v " > 4 8 , 2 1 3 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Connections < / d i v > < d i v c l a s s = " v " > 1 7 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Subscriptions < / d i v > < d i v c l a s s = " v " > 4 2 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > DB size < / d i v > < d i v c l a s s = " v " > 3 1 2 M B < / d i v > < / d i v >
< / d i v >
< div class = "card" > < table >
< tr > < th > Kind < / t h > < t h > D e s c r i p t i o n < / t h > < t h > E v e n t s ( 2 4 h ) < / t h > < / t r >
< tr > < td > 1 < / t d > < t d > S h o r t t e x t n o t e s < / t d > < t d > 1 , 8 2 4 < / t d > < / t r >
< tr > < td > 0 < / t d > < t d > P r o f i l e m e t a d a t a < / t d > < t d > 9 6 < / t d > < / t r >
< tr > < td > 7 < / t d > < t d > R e a c t i o n s < / t d > < t d > 3 , 4 1 2 < / t d > < / t r >
< tr > < td > 4 < / t d > < t d > E n c r y p t e d D M s < / t d > < t d > 5 8 < / t d > < / t r >
< / t a b l e > < / d i v > ` ) ,
searxng : ( ) => demoAppShell ( 'SearXNG' , 'Privacy-respecting metasearch' , '/assets/img/app-icons/searxng.png' , `
< div class = "card" style = "margin-bottom:18px;text-align:center;padding:28px" >
< input placeholder = "Search privately…" style = "width:100%;max-width:480px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.14);border-radius:10px;padding:12px 16px;color:#f2f2f4;font-size:15px;outline:none" >
< div class = "k" style = "margin-top:12px" > No queries logged · no profiling · results proxied < / d i v >
< / d i v >
< div class = "grid" >
< div class = "card" > < div class = "k" > Engines enabled < / d i v > < d i v c l a s s = " v " > 2 8 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Queries today < / d i v > < d i v c l a s s = " v " > 6 4 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Avg response < / d i v > < d i v c l a s s = " v " > 0 . 8 s < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Tracking < / d i v > < d i v c l a s s = " v " > < s p a n c l a s s = " b a d g e " > Z e r o < / s p a n > < / d i v > < / d i v >
< / d i v > ` ) ,
'uptime-kuma' : ( ) => demoAppShell ( 'Uptime Kuma' , 'Service monitoring' , '/assets/img/app-icons/uptime-kuma.webp' , `
< div class = "grid" >
< div class = "card" > < div class = "k" > Monitors < / d i v > < d i v c l a s s = " v " > 8 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Up < / d i v > < d i v c l a s s = " v " > 8 < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Avg uptime ( 30 d ) < / d i v > < d i v c l a s s = " v " > 9 9 . 9 4 % < / d i v > < / d i v >
< div class = "card" > < div class = "k" > Incidents < / d i v > < d i v c l a s s = " v " > 1 < / d i v > < / d i v >
< / d i v >
< div class = "card" > < table >
< tr > < th > Monitor < / t h > < t h > S t a t u s < / t h > < t h > U p t i m e < / t h > < t h > P i n g < / t h > < / t r >
< tr > < td > Bitcoin RPC < / t d > < t d > < s p a n c l a s s = " b a d g e " > U p < / s p a n > < / t d > < t d > 1 0 0 % < / t d > < t d > 4 m s < / t d > < / t r >
< tr > < td > LND REST < / t d > < t d > < s p a n c l a s s = " b a d g e " > U p < / s p a n > < / t d > < t d > 9 9 . 9 % < / t d > < t d > 7 m s < / t d > < / t r >
< tr > < td > Electrum server < / t d > < t d > < s p a n c l a s s = " b a d g e " > U p < / s p a n > < / t d > < t d > 9 9 . 8 % < / t d > < t d > 1 1 m s < / t d > < / t r >
< tr > < td > Tor hidden service < / t d > < t d > < s p a n c l a s s = " b a d g e " > U p < / s p a n > < / t d > < t d > 9 9 . 7 % < / t d > < t d > 3 8 0 m s < / t d > < / t r >
< tr > < td > Mempool web < / t d > < t d > < s p a n c l a s s = " b a d g e " > U p < / s p a n > < / t d > < t d > 1 0 0 % < / t d > < t d > 6 m s < / t d > < / t r >
< / t a b l e > < / d i v > ` ) ,
}
for ( const [ id , page ] of Object . entries ( DEMO _APP _PAGES ) ) {
app . get ( [ ` /app/ ${ id } ` , ` /app/ ${ id } / ` ] , ( _req , res ) => res . type ( 'html' ) . send ( page ( ) ) )
}
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
// 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/<id>[/] reaches here.
app . get ( [ '/app/:id' , '/app/:id/' ] , ( req , res ) => {
const id = String ( req . params . id || '' ) . replace ( /[^a-z0-9._-]/gi , '' )
const title = id . replace ( /[-_]/g , ' ' ) . replace ( /\b\w/g , c => c . toUpperCase ( ) )
res . type ( 'html' ) . send ( ` <!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
< meta name = "viewport" content = "width=device-width,initial-scale=1" > < title > $ { title } < / t i t l e >
< style > * { margin : 0 ; padding : 0 ; box - sizing : border - box } html , body { height : 100 % }
2026-06-22 13:55:50 -04:00
body { background : # 000 ; color : # f2f2f4 ; font - family : system - ui , - apple - system , Segoe UI , Roboto , sans - serif ; display : flex ; align - items : center ; justify - content : center ;
background - image : radial - gradient ( 900 px 400 px at 50 % - 10 % , rgba ( 247 , 147 , 26 , . 07 ) , transparent 70 % ) }
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
. card { text - align : center ; padding : 48 px 40 px ; max - width : 380 px }
2026-06-22 13:55:50 -04:00
img { width : 84 px ; height : 84 px ; border - radius : 20 px ; object - fit : cover ; margin - bottom : 20 px ; background : rgba ( 255 , 255 , 255 , . 04 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , . 08 ) }
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
h1 { font - size : 22 px ; font - weight : 650 ; margin - bottom : 8 px }
2026-06-22 13:55:50 -04:00
p { color : # 8 a8f9a ; font - size : 14 px ; line - height : 1.5 }
. tag { margin - top : 18 px ; display : inline - block ; font - size : 12 px ; color : # f7931a ; border : 1 px solid rgba ( 247 , 147 , 26 , . 3 ) ; background : rgba ( 247 , 147 , 26 , . 1 ) ; padding : 5 px 12 px ; border - radius : 999 px }
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
< / s t y l e > < / h e a d > < b o d y > < d i v c l a s s = " c a r d " >
< img src = "/assets/img/app-icons/${id}.png" onerror = "this.onerror=null;this.src='/assets/icon/favico-black.svg'" alt = "" >
< h1 > $ { title } < / h 1 >
2026-06-22 12:39:33 -04:00
< p > Not available in the demo . < br > This app runs fully on a real Archipelago node . < / p >
< div class = "tag" > Demo < / d i v >
feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes
App launching (DEMO):
- resolveAppUrl routes every app to its demo target: mock UIs for Bitcoin Core,
ElectrumX, Fedimint (served by the backend), IndeeHub → iframe indee.tx1138.com,
Mempool → mempool.space/testnet (new tab); all others → a generic "Demo preview"
notice page.
- Non-demoable apps show a disabled "No demo" install button (marketplace details,
app grid, featured apps).
Onboarding:
- Demo treats the visitor as fully set up so the onboarding WIZARD (seed/identity)
is never forced; the welcome intro still replays per day. Intro CTA goes straight
to login; wizard entry points + login restart-onboarding link hidden in demo.
Network:
- federation.list-nodes now returns 12 trusted/federated nodes (9 trusted, 3
observer); transport.peers already at 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:26:35 -04:00
< / d i v > < / b o d y > < / h t m l > ` )
} )
2026-01-24 22:59:20 +00:00
// Health check
app . get ( '/health' , ( req , res ) => {
res . status ( 200 ) . send ( 'healthy' )
} )
2026-06-22 09:28:05 -04:00
// ───────────────────────────────────────────────────────────────────────────
// Per-session state isolation (DEMO multi-visitor sandbox)
//
// Every mutable global (mockData, walletState, userState, mockState,
// bitcoinRelayMockState) and the filebrowser file store is partitioned per
// visitor. Handlers keep referring to those names unchanged — the names are
// Proxies that forward to the current request's store, resolved via
// AsyncLocalStorage. Outside DEMO (or outside a request, e.g. at startup) they
// resolve to a single shared `defaultStore`, so classic single-user behaviour
// is byte-for-byte preserved.
// ───────────────────────────────────────────────────────────────────────────
const stateContext = new AsyncLocalStorage ( )
// Build a fresh, fully-isolated state bundle from the pristine seeds.
function makeSessionStore ( ) {
const md = structuredClone ( SEED _MOCKDATA )
// No real runtime in DEMO → package list is the curated static app set.
md [ 'package-data' ] = structuredClone ( staticDevApps )
return {
mockData : md ,
walletState : structuredClone ( SEED _WALLET ) ,
userState : seedUserState ( ) ,
mockState : seedMockState ( ) ,
bitcoinRelayMockState : structuredClone ( SEED _BTCRELAY ) ,
files : { tree : structuredClone ( SEED _FILES ) , contents : structuredClone ( SEED _FILE _CONTENTS ) , bytes : 0 } ,
sockets : new Set ( ) ,
lastSeen : Date . now ( ) ,
}
}
// The shared store used in single-user mode and for any work outside a request.
const defaultStore = makeSessionStore ( )
function currentStore ( ) {
return stateContext . getStore ( ) || defaultStore
}
// A Proxy whose every operation is delegated to currentStore()[bucket], so the
// existing handler code (`mockData['package-data']`, `walletState.x += n`, …)
// transparently reads/writes the right visitor's state.
function sessionBucketProxy ( bucket ) {
const target = ( ) => currentStore ( ) [ bucket ]
return new Proxy ( Object . create ( null ) , {
get : ( _t , k ) => target ( ) [ k ] ,
set : ( _t , k , v ) => { target ( ) [ k ] = v ; return true } ,
has : ( _t , k ) => k in target ( ) ,
deleteProperty : ( _t , k ) => { delete target ( ) [ k ] ; return true } ,
ownKeys : ( ) => Reflect . ownKeys ( target ( ) ) ,
getOwnPropertyDescriptor : ( _t , k ) => {
const d = Object . getOwnPropertyDescriptor ( target ( ) , k )
if ( d ) d . configurable = true
return d
} ,
defineProperty : ( _t , k , d ) => { Object . defineProperty ( target ( ) , k , d ) ; return true } ,
getPrototypeOf : ( ) => Object . prototype ,
} )
}
const mockData = sessionBucketProxy ( 'mockData' )
const walletState = sessionBucketProxy ( 'walletState' )
const userState = sessionBucketProxy ( 'userState' )
const mockState = sessionBucketProxy ( 'mockState' )
2026-07-15 05:17:01 -04:00
// Seed for the per-session Tor services demo state (tor.list-services /
// tor.create-service / tor.delete-service round-trip against this).
function defaultTorServices ( ) {
return [
{ name : 'archipelago' , local _port : 5678 , onion _address : 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion' , enabled : true , unauthenticated : false , protocol : false } ,
{ name : 'bitcoin' , local _port : 8333 , onion _address : 'btcmockxj4k5pnw7hv8qz9jcbr2dwefys6ockqzf1mzjlvxot5ioad.onion' , enabled : true , unauthenticated : false , protocol : false } ,
{ name : 'lnd' , local _port : 9735 , onion _address : 'lndmockab3c4def5ghi6jkl7mno8pqr9stu0vwx1yz2ab3c4def5ghi.onion' , enabled : true , unauthenticated : false , protocol : false } ,
{ name : 'electrs' , local _port : 50001 , onion _address : 'elecmockyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba1xyz9wvu8tsr7q.onion' , enabled : true , unauthenticated : true , protocol : false } ,
{ name : 'nostr-relay' , local _port : 7777 , onion _address : null , enabled : false , unauthenticated : true , protocol : false } ,
]
}
2026-06-22 09:28:05 -04:00
const bitcoinRelayMockState = sessionBucketProxy ( 'bitcoinRelayMockState' )
// Demo session lifecycle: keyed by the `demo_sid` cookie, capped, idle-reaped.
const demoSessions = new Map ( ) // sid -> store
const DEMO _SESSION _TTL _MS = Number ( process . env . DEMO _SESSION _TTL _MS ) || 45 * 60 * 1000
const DEMO _MAX _SESSIONS = Number ( process . env . DEMO _MAX _SESSIONS ) || 500
function resolveSessionStore ( req , res ) {
let sid = req . cookies ? . demo _sid
if ( ! sid || ! demoSessions . has ( sid ) ) {
// Cap concurrent sessions: evict the oldest if we're at the limit.
if ( demoSessions . size >= DEMO _MAX _SESSIONS ) {
let oldestSid = null , oldest = Infinity
for ( const [ k , s ] of demoSessions ) if ( s . lastSeen < oldest ) { oldest = s . lastSeen ; oldestSid = k }
if ( oldestSid ) { reapSession ( oldestSid ) }
}
sid = crypto . randomUUID ( )
demoSessions . set ( sid , makeSessionStore ( ) )
res . cookie ( 'demo_sid' , sid , { httpOnly : true , sameSite : 'lax' , maxAge : DEMO _SESSION _TTL _MS } )
}
const store = demoSessions . get ( sid )
store . lastSeen = Date . now ( )
store . sid = sid
return store
}
// Resolve the session store for a WebSocket upgrade request. The HTTP layer has
// already issued the `demo_sid` cookie by the time the socket connects; if it is
// somehow absent we fall back to a fresh ephemeral store (no cookie to set here).
function wsStoreForRequest ( req ) {
const raw = req . headers ? . cookie || ''
const m = raw . match ( /(?:^|;\s*)demo_sid=([^;]+)/ )
const sid = m && m [ 1 ]
if ( sid && demoSessions . has ( sid ) ) {
const store = demoSessions . get ( sid )
store . lastSeen = Date . now ( )
return store
}
const store = makeSessionStore ( )
if ( sid ) demoSessions . set ( sid , store )
return store
}
function reapSession ( sid ) {
const store = demoSessions . get ( sid )
if ( ! store ) return
for ( const ws of store . sockets ) { try { ws . close ( 4000 , 'session expired' ) } catch { /* ignore */ } }
demoSessions . delete ( sid )
}
if ( DEMO ) {
setInterval ( ( ) => {
const now = Date . now ( )
for ( const [ sid , store ] of demoSessions ) {
if ( now - store . lastSeen > DEMO _SESSION _TTL _MS ) reapSession ( sid )
}
} , 60 * 1000 ) . unref ? . ( )
}
2026-01-24 22:59:20 +00:00
// WebSocket endpoint
const server = http . createServer ( app )
const wss = new WebSocketServer ( { server , path : '/ws/db' } )
wss . on ( 'connection' , ( ws , req ) => {
console . log ( '[WebSocket] Client connected from' , req . socket . remoteAddress )
2026-06-22 09:28:05 -04:00
// Attach this socket to the visitor's session store so broadcasts only reach
// that visitor. In non-DEMO mode every socket joins the single default store.
const wsStore = DEMO ? wsStoreForRequest ( req ) : defaultStore
wsStore . sockets . add ( ws )
2026-01-24 22:59:20 +00:00
// Set up ping/pong to keep connection alive
const pingInterval = setInterval ( ( ) => {
if ( ws . readyState === 1 ) { // OPEN
try {
ws . ping ( )
} catch ( err ) {
console . error ( '[WebSocket] Ping error:' , err )
clearInterval ( pingInterval )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-01-24 22:59:20 +00:00
}
} else {
clearInterval ( pingInterval )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-01-24 22:59:20 +00:00
}
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
} , 25000 ) // Ping every 25 seconds
2026-01-24 22:59:20 +00:00
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
// 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.
2026-03-07 22:36:45 +00:00
const heartbeatInterval = setInterval ( ( ) => {
if ( ws . readyState === 1 ) {
try {
2026-03-07 23:22:30 +00:00
ws . send ( JSON . stringify ( { type : 'heartbeat' , rev : Date . now ( ) } ) )
2026-03-07 22:36:45 +00:00
} catch { /* ignore */ }
}
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
} , 20000 )
2026-03-07 22:36:45 +00:00
2026-06-22 09:28:05 -04:00
// Send initial data immediately (this visitor's store, not the global proxy —
// there is no request context inside the WS connection handler).
2026-01-24 22:59:20 +00:00
try {
ws . send ( JSON . stringify ( {
type : 'initial' ,
2026-06-22 09:28:05 -04:00
data : wsStore . mockData ,
2026-01-24 22:59:20 +00:00
} ) )
console . log ( '[WebSocket] Initial data sent' )
} catch ( err ) {
console . error ( '[WebSocket] Error sending initial data:' , err )
}
ws . on ( 'pong' , ( ) => {
// Client responded to ping, connection is alive
} )
ws . on ( 'message' , ( message ) => {
// Handle incoming messages if needed
try {
const data = JSON . parse ( message . toString ( ) )
console . log ( '[WebSocket] Received message:' , data )
} catch ( err ) {
console . error ( '[WebSocket] Error parsing message:' , err )
}
} )
ws . on ( 'close' , ( code , reason ) => {
console . log ( '[WebSocket] Client disconnected' , { code , reason : reason . toString ( ) } )
clearInterval ( pingInterval )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-06-22 09:28:05 -04:00
wsStore . sockets . delete ( ws )
2026-01-24 22:59:20 +00:00
} )
ws . on ( 'error' , ( error ) => {
console . error ( '[WebSocket Error]' , error )
clearInterval ( pingInterval )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-06-22 09:28:05 -04:00
wsStore . sockets . delete ( ws )
2026-01-24 22:59:20 +00:00
} )
} )
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
// 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.
2026-06-22 10:53:05 -04:00
async function hydrateRealTestnetTxids ( ) {
if ( ! DEMO ) return
try {
const ctrl = new AbortController ( )
const t = setTimeout ( ( ) => ctrl . abort ( ) , 4000 )
fix(demo): mock backend overhaul — real media, correct shapes, mempool.guide
- Serve committed demo/content (music/photos/documents) alongside demo/files
drop-ins, normalizing top-level folder names; Dockerfile now COPYs it. This
is why Cloud music failed: the real library sat in demo/content while the
loader only read the empty demo/files.
- Drop unbacked seeded Videos; runtime WAV/SVG fallback for any seeded media
without a real file so playback never hits 'no supported source'.
- monitoring.current/history/alerts/alert-rules/export rewritten to the exact
MetricSnapshot shapes Monitoring.vue expects (epoch-second timestamps,
containers with limits/block IO, rpc_latency_ms, ws_connections).
- streaming.list-services + configure-service (Networking Profits page was
erroring with Method not found).
- server.get-state snapshot so the 30s resync / reconnect path stops erroring.
- lnd-connect-info now returns cert/macaroon base64url + grpc/rest ports (the
lnd-ui shell only renders the wallet QR + details when they're present);
LND REST balance endpoints for the shell's new balance cards.
- Fix broken icon paths (lnd.svg → lnd.png and 15 others) against real assets.
- containers-scanned: true so app cards never sit on 'Checking…'.
- 8 new installed demo apps with placeholder dashboard UIs (btcpay-server,
grafana, nextcloud, jellyfin, vaultwarden, nostr-rs-relay, searxng,
uptime-kuma) via the previously unused demoAppShell.
- WS heartbeat 45s → 20s (+ping 25s) so idle-timeout proxies in front of the
public demo stop killing the socket (the 'always reconnecting' report).
- nginx demo proxy: mempool.space → mempool.guide (mempool.space blocks the
proxied embed) + WebSocket upgrade passthrough; txid hydration follows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 03:39:01 +01:00
const r = await fetch ( 'https://mempool.guide/api/mempool/recent' , { signal : ctrl . signal } )
2026-06-22 10:53:05 -04:00
clearTimeout ( t )
if ( ! r . ok ) return
const recent = await r . json ( )
const txids = ( Array . isArray ( recent ) ? recent : [ ] ) . map ( x => x . txid ) . filter ( Boolean )
if ( ! txids . length ) return
SEED _WALLET . transactions . forEach ( ( tx , i ) => { if ( txids [ i ] ) tx . tx _hash = txids [ i ] } )
// Also patch the already-built default store (sessions clone from SEED at creation).
defaultStore . walletState . transactions . forEach ( ( tx , i ) => { if ( txids [ i ] ) tx . tx _hash = txids [ i ] } )
console . log ( ` [Demo] Hydrated ${ Math . min ( txids . length , SEED _WALLET . transactions . length ) } real testnet txids ` )
} catch { /* offline — keep mock hashes */ }
}
2026-01-24 22:59:20 +00:00
server . listen ( PORT , '0.0.0.0' , async ( ) => {
const runtime = await isContainerRuntimeAvailable ( )
2026-06-22 10:53:05 -04:00
2026-01-27 23:06:18 +00:00
// Initialize package data from Docker
await initializePackageData ( )
2026-06-22 10:53:05 -04:00
await hydrateRealTestnetTxids ( )
2026-01-24 22:59:20 +00:00
console . log ( `
╔ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╗
║ ║
║ 🚀 Archipelago Mock Backend Server ║
║ ║
║ RPC : http : //localhost:${PORT}/rpc/v1 ║
║ WebSocket : ws : //localhost:${PORT}/ws/db ║
║ ║
║ Dev Mode : $ { DEV _MODE . padEnd ( 47 ) } ║
║ Setup : $ { userState . setupComplete ? '✅ Complete' : '❌ Not done' . padEnd ( 47 ) } ║
║ Onboarding : $ { userState . onboardingComplete ? '✅ Complete' : '❌ Not done' . padEnd ( 46 ) } ║
║ ║
║ Mock Password : $ { MOCK _PASSWORD . padEnd ( 40 ) } ║
║ ║
2026-03-18 21:06:14 +00:00
║ Container Runtime : $ { runtime . available ? ` ✅ ${ runtime . runtime } ` . padEnd ( 40 ) : '⏭️ Simulation mode' . padEnd ( 40 ) } ║
2026-03-09 13:03:53 +00:00
║ Claude API Key : $ { process . env . ANTHROPIC _API _KEY ? '✅ Set (' + process . env . ANTHROPIC _API _KEY . slice ( 0 , 12 ) + '...)' : '❌ Not set (chat disabled)' . padEnd ( 40 ) } ║
2026-01-24 22:59:20 +00:00
║ ║
╚ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╝
` )
console . log ( 'Mock backend is running. Press Ctrl+C to stop.\n' )
2026-03-09 13:03:53 +00:00
// 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 */ }
}
2026-01-27 23:06:18 +00:00
2026-02-18 08:30:12 +00:00
// Periodically update package data from Docker (merge with static dev apps)
2026-03-07 20:53:02 +00:00
// Only poll if container runtime is available (avoids log spam in demo/Docker deployments)
if ( runtime . available ) {
setInterval ( async ( ) => {
const dockerApps = await getDockerContainers ( )
mockData [ 'package-data' ] = mergePackageData ( dockerApps )
// Broadcast update to connected clients
broadcastUpdate ( [
{
op : 'replace' ,
path : '/package-data' ,
value : mockData [ 'package-data' ]
}
] )
} , 5000 ) // Update every 5 seconds
}
2026-01-24 22:59:20 +00:00
} )
process . on ( 'SIGINT' , ( ) => {
console . log ( '\n\nShutting down mock backend...' )
server . close ( ( ) => {
console . log ( 'Server stopped.' )
process . exit ( 0 )
} )
} )