fix(demo): iframe mempool+indeehub directly, serve real UIs statically, AIUI canned

- 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/<id>/ (express.static)
  so their bundled assets (qrcode.js, css, bg images) resolve; /app/<id>/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) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-06-22 14:45:04 -04:00
parent cf5f6d021a
commit b99c4a604f
4 changed files with 57 additions and 79 deletions

View File

@ -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/<id>/) 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.

View File

@ -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/<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'],
[['/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/<id>/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/<dir>/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({

View File

@ -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<string, string> = {
// 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<string, string> = {
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<string, string> = {
}
/**
* 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? */

View File

@ -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',