From b99c4a604fc4c01ddc2d226437835f64c4b0a9df Mon Sep 17 00:00:00 2001 From: archipelago Date: Mon, 22 Jun 2026 14:45:04 -0400 Subject: [PATCH] fix(demo): iframe mempool+indeehub directly, serve real UIs statically, AIUI canned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mempool and IndeeHub load their real site directly in the iframe (reverted the proxy/new-tab — per request "use https://indee.tx1138.com/"). - Real app UIs now served as whole static dirs under /app// (express.static) so their bundled assets (qrcode.js, css, bg images) resolve; /app//assets/* redirect to the frontend's shared assets. Fixes the console 404 cascade. - Bitcoin Core/Knots: register rpc/v1 + bitcoin-rpc on their paths (relay-status no longer 404s); per-impl bitcoin-status preserved. - AIUI chat returns a fixed line in demo ("Not available in demo, check out the previous chats to experience AIUI") instead of calling Claude — no key spend. - Add /api/app-catalog (serves the baked catalog) to stop that 404. Co-Authored-By: Claude Opus 4.8 (1M context) --- neode-ui/docker/nginx-demo.conf | 13 --- neode-ui/mock-backend.js | 107 +++++++++++------------ neode-ui/src/composables/useDemoIntro.ts | 15 ++-- neode-ui/vite.config.ts | 1 - 4 files changed, 57 insertions(+), 79 deletions(-) diff --git a/neode-ui/docker/nginx-demo.conf b/neode-ui/docker/nginx-demo.conf index e4fda720..5c55bde8 100644 --- a/neode-ui/docker/nginx-demo.conf +++ b/neode-ui/docker/nginx-demo.conf @@ -94,19 +94,6 @@ http { proxy_request_buffering off; } - # IndeeHub: reverse-proxy the real site same-origin and strip its - # X-Frame-Options / CSP so it can load inside the in-app iframe. - location /app/indeedhub/ { - proxy_pass https://indee.tx1138.com/; - proxy_http_version 1.1; - proxy_set_header Host indee.tx1138.com; - proxy_ssl_server_name on; - proxy_set_header Accept-Encoding ""; - proxy_hide_header X-Frame-Options; - proxy_hide_header Content-Security-Policy; - proxy_hide_header Content-Security-Policy-Report-Only; - } - # Proxy every other app UI (/app//) to the mock backend, which serves # the per-app mock UIs (bitcoin-ui, electrumx, lnd, fedimint) and the # generic "Not available in the demo" notice for the rest. diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index 4278613f..afbfb038 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -1055,22 +1055,21 @@ function demoFederationNodes() { }) } -// ElectrumX: serve the real electrs-ui shell (its qrcode.js sibling + the -// /electrs-status endpoint it polls are served below with dummy data). -app.get(['/app/electrumx/', '/app/electrs/', '/app/archy-electrs-ui/'], async (_req, res) => { - try { - const html = await fs.readFile(path.join(__dirname, '..', 'docker', 'electrs-ui', 'index.html'), 'utf8') - res.type('html').send(html) - } catch (error) { - res.status(500).type('text/plain').send(`Unable to load ElectrumX UI mock: ${error.message}`) - } -}) -app.get(['/app/electrumx/qrcode.js', '/app/electrs/qrcode.js', '/app/archy-electrs-ui/qrcode.js'], async (_req, res) => { - try { - const js = await fs.readFile(path.join(__dirname, '..', 'docker', 'electrs-ui', 'qrcode.js'), 'utf8') - res.type('application/javascript').send(js) - } catch { res.status(404).end() } -}) +// Serve each real registry UI's WHOLE directory at its /app// 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'], + [['/app/lnd', '/app/lnd-ui', '/app/archy-lnd-ui', '/app/thunderhub'], 'lnd-ui'], + [['/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//assets/... resolve to the frontend's shared assets. +app.get(/^\/app\/[^/]+\/assets\/(.*)$/, (req, res) => res.redirect(302, '/assets/' + req.params[0])) + // Dummy status for both the electrs-ui shell and the in-app ElectrumX sync screen. app.get('/electrs-status', (_req, res) => { res.json({ @@ -1087,22 +1086,7 @@ app.get('/electrs-status', (_req, res) => { }) }) -// Serve a real registry UI shell (docker//index.html), filled by the -// dummy endpoints each shell polls (defined below). This gives accurate UX. -function serveDockerUI(dir) { - return async (_req, res) => { - try { - const html = await fs.readFile(path.join(__dirname, '..', 'docker', dir, 'index.html'), 'utf8') - res.type('html').send(html) - } catch (e) { - res.status(500).type('text/plain').send(`Unable to load ${dir} mock: ${e.message}`) - } - } -} - -// ── Bitcoin Core + Knots: the real bitcoin-ui shell, per-implementation status ─ -app.get('/app/bitcoin-core/', serveDockerUI('bitcoin-ui')) -app.get('/app/bitcoin-knots/', serveDockerUI('bitcoin-ui')) +// ── Bitcoin Core + Knots: per-implementation status (shell served statically) ─ function bitcoinStatusPayload(impl) { const net = mockBitcoinNetworkInfo() net.subversion = impl === 'knots' ? '/Satoshi:28.1.0/Knots:20250305/' : '/Satoshi:28.4.0/' @@ -1123,8 +1107,7 @@ function bitcoinStatusPayload(impl) { 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'))) -// ── LND: the real lnd-ui shell + the endpoints it polls (same-origin) ──────── -app.get(['/app/lnd/', '/app/lnd-ui/', '/app/archy-lnd-ui/', '/app/thunderhub/'], serveDockerUI('lnd-ui')) +// ── LND: the endpoints the lnd-ui shell polls (shell served statically) ────── function lndGetinfo() { return { alias: 'archipelago-lnd', identity_pubkey: '02' + randomHex(32), @@ -1152,6 +1135,13 @@ app.get('/lnd-connect-info', (_req, res) => res.json({ grpcReachable: true, restReachable: true, tor_onion: 'lnd' + randomHex(16) + '.onion', error: null, })) +// 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' }) } +}) app.get('/api/container/logs', (_req, res) => res.json({ logs: [ '[INF] LND: Version 0.18.3-beta commit=v0.18.3-beta', @@ -1162,29 +1152,9 @@ app.get('/api/container/logs', (_req, res) => res.json({ ].join('\n'), })) -// ── Fedimint: the real fedimint-ui shell (static) ──────────────────────────── -app.get(['/app/fedimint/', '/app/fedimintd/'], serveDockerUI('fedimint-ui')) +app.get('/app/bitcoin-ui/bitcoin-status', (_req, res) => res.json(bitcoinStatusPayload('knots'))) -app.get('/app/bitcoin-ui/bitcoin-status', (_req, res) => { - res.json({ - ok: true, - stale: false, - updated_at_ms: Date.now(), - error: null, - blockchain_info: mockBitcoinBlockchainInfo(), - network_info: mockBitcoinNetworkInfo(), - 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.post('/app/bitcoin-ui/bitcoin-rpc/', (req, res) => { +app.post(['/app/bitcoin-ui/bitcoin-rpc/', '/app/bitcoin-core/bitcoin-rpc/', '/app/bitcoin-knots/bitcoin-rpc/'], (req, res) => { const { id = 'bitcoin-ui', method } = req.body || {} const results = { getblockchaininfo: mockBitcoinBlockchainInfo(), @@ -1206,7 +1176,7 @@ app.post('/app/bitcoin-ui/bitcoin-rpc/', (req, res) => { res.json({ id, result: results[method], error: null }) }) -app.post('/app/bitcoin-ui/rpc/v1', (req, res) => { +app.post(['/app/bitcoin-ui/rpc/v1', '/app/bitcoin-core/rpc/v1', '/app/bitcoin-knots/rpc/v1'], (req, res) => { const { method, params = {} } = req.body || {} const now = new Date().toISOString() switch (method) { @@ -3781,6 +3751,29 @@ function demoNodeContext() { // ============================================================================= app.post('/aiui/api/claude/*', async (req, res) => { + // 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}\ndata: ${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() + } const apiKey = process.env.ANTHROPIC_API_KEY if (!apiKey) { return res.status(500).json({ diff --git a/neode-ui/src/composables/useDemoIntro.ts b/neode-ui/src/composables/useDemoIntro.ts index 393b3b57..4bc4533e 100644 --- a/neode-ui/src/composables/useDemoIntro.ts +++ b/neode-ui/src/composables/useDemoIntro.ts @@ -53,15 +53,14 @@ export function clearDemoIntroSeen(): void { // external site). Everything else shows "No demo" on a disabled install button // and is not launchable. const DEMO_EXTERNAL_URLS: Record = { - // Real, public sites opened in a NEW TAB (they block iframing). + // Real, public sites loaded directly in the in-app iframe. + indeedhub: 'https://indee.tx1138.com/', mempool: 'https://mempool.space/testnet', 'mempool-web': 'https://mempool.space/testnet', } -// Apps with a same-origin URL loaded in the in-app iframe — either a mock UI -// served by the demo backend, or a header-stripping reverse-proxy (IndeeHub). +// Apps with a same-origin mock UI served by the demo backend. const DEMO_MOCK_UI: Record = { - indeedhub: '/app/indeedhub/', 'bitcoin-knots': '/app/bitcoin-knots/', 'bitcoin-core': '/app/bitcoin-core/', bitcoin: '/app/bitcoin-core/', @@ -79,11 +78,11 @@ const DEMO_MOCK_UI: Record = { } /** - * Whether a demo app opens in a new tab. Mempool.space sets X-Frame-Options and - * cannot be iframed, so it opens in a new tab; IndeeHub still loads in-iframe. + * Whether a demo app opens in a new tab. Nothing does — IndeeHub and Mempool + * both load their real site directly in the in-app iframe. */ -export function isDemoExternal(appId: string): boolean { - return appId === 'mempool' || appId === 'mempool-web' +export function isDemoExternal(_appId: string): boolean { + return false } /** Can this app be launched/installed in the demo? */ diff --git a/neode-ui/vite.config.ts b/neode-ui/vite.config.ts index 57c98183..67149bd7 100644 --- a/neode-ui/vite.config.ts +++ b/neode-ui/vite.config.ts @@ -161,7 +161,6 @@ export default defineConfig({ '/electrs-status': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false }, '/proxy': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false }, '/lnd-connect-info': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false }, - '/app/indeedhub': { target: 'https://indee.tx1138.com', changeOrigin: true, secure: false, rewrite: (p: string) => p.replace(/^\/app\/indeedhub/, '') }, // Serve the node's deployed AIUI same-origin like production (set VITE_AIUI_URL=/aiui/) '/aiui': { target: process.env.AIUI_PROXY_TARGET || 'http://127.0.0.1:80',