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:
parent
cf5f6d021a
commit
b99c4a604f
@ -94,19 +94,6 @@ http {
|
|||||||
proxy_request_buffering off;
|
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
|
# 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
|
# the per-app mock UIs (bitcoin-ui, electrumx, lnd, fedimint) and the
|
||||||
# generic "Not available in the demo" notice for the rest.
|
# generic "Not available in the demo" notice for the rest.
|
||||||
|
|||||||
@ -1055,22 +1055,21 @@ function demoFederationNodes() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ElectrumX: serve the real electrs-ui shell (its qrcode.js sibling + the
|
// Serve each real registry UI's WHOLE directory at its /app/<id>/ path, so the
|
||||||
// /electrs-status endpoint it polls are served below with dummy data).
|
// shell's bundled assets (qrcode.js, css, bg images, icons) resolve via their
|
||||||
app.get(['/app/electrumx/', '/app/electrs/', '/app/archy-electrs-ui/'], async (_req, res) => {
|
// relative paths. The data endpoints each shell polls are mocked separately.
|
||||||
try {
|
const DOCKER_UI = path.join(__dirname, '..', 'docker')
|
||||||
const html = await fs.readFile(path.join(__dirname, '..', 'docker', 'electrs-ui', 'index.html'), 'utf8')
|
for (const [prefixes, dir] of [
|
||||||
res.type('html').send(html)
|
[['/app/bitcoin-core', '/app/bitcoin-knots', '/app/bitcoin-ui'], 'bitcoin-ui'],
|
||||||
} catch (error) {
|
[['/app/electrumx', '/app/electrs', '/app/archy-electrs-ui'], 'electrs-ui'],
|
||||||
res.status(500).type('text/plain').send(`Unable to load ElectrumX UI mock: ${error.message}`)
|
[['/app/lnd', '/app/lnd-ui', '/app/archy-lnd-ui', '/app/thunderhub'], 'lnd-ui'],
|
||||||
}
|
[['/app/fedimint', '/app/fedimintd'], 'fedimint-ui'],
|
||||||
})
|
]) {
|
||||||
app.get(['/app/electrumx/qrcode.js', '/app/electrs/qrcode.js', '/app/archy-electrs-ui/qrcode.js'], async (_req, res) => {
|
for (const p of prefixes) app.use(p, express.static(path.join(DOCKER_UI, dir)))
|
||||||
try {
|
}
|
||||||
const js = await fs.readFile(path.join(__dirname, '..', 'docker', 'electrs-ui', 'qrcode.js'), 'utf8')
|
// Shells that reference /app/<id>/assets/... resolve to the frontend's shared assets.
|
||||||
res.type('application/javascript').send(js)
|
app.get(/^\/app\/[^/]+\/assets\/(.*)$/, (req, res) => res.redirect(302, '/assets/' + req.params[0]))
|
||||||
} catch { res.status(404).end() }
|
|
||||||
})
|
|
||||||
// Dummy status for both the electrs-ui shell and the in-app ElectrumX sync screen.
|
// Dummy status for both the electrs-ui shell and the in-app ElectrumX sync screen.
|
||||||
app.get('/electrs-status', (_req, res) => {
|
app.get('/electrs-status', (_req, res) => {
|
||||||
res.json({
|
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
|
// ── Bitcoin Core + Knots: per-implementation status (shell served statically) ─
|
||||||
// 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'))
|
|
||||||
function bitcoinStatusPayload(impl) {
|
function bitcoinStatusPayload(impl) {
|
||||||
const net = mockBitcoinNetworkInfo()
|
const net = mockBitcoinNetworkInfo()
|
||||||
net.subversion = impl === 'knots' ? '/Satoshi:28.1.0/Knots:20250305/' : '/Satoshi:28.4.0/'
|
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-core/bitcoin-status', (_req, res) => res.json(bitcoinStatusPayload('core')))
|
||||||
app.get('/app/bitcoin-knots/bitcoin-status', (_req, res) => res.json(bitcoinStatusPayload('knots')))
|
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) ────────
|
// ── LND: the endpoints the lnd-ui shell polls (shell served statically) ──────
|
||||||
app.get(['/app/lnd/', '/app/lnd-ui/', '/app/archy-lnd-ui/', '/app/thunderhub/'], serveDockerUI('lnd-ui'))
|
|
||||||
function lndGetinfo() {
|
function lndGetinfo() {
|
||||||
return {
|
return {
|
||||||
alias: 'archipelago-lnd', identity_pubkey: '02' + randomHex(32),
|
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,
|
grpcReachable: true, restReachable: true,
|
||||||
tor_onion: 'lnd' + randomHex(16) + '.onion', error: null,
|
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({
|
app.get('/api/container/logs', (_req, res) => res.json({
|
||||||
logs: [
|
logs: [
|
||||||
'[INF] LND: Version 0.18.3-beta commit=v0.18.3-beta',
|
'[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'),
|
].join('\n'),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// ── Fedimint: the real fedimint-ui shell (static) ────────────────────────────
|
app.get('/app/bitcoin-ui/bitcoin-status', (_req, res) => res.json(bitcoinStatusPayload('knots')))
|
||||||
app.get(['/app/fedimint/', '/app/fedimintd/'], serveDockerUI('fedimint-ui'))
|
|
||||||
|
|
||||||
app.get('/app/bitcoin-ui/bitcoin-status', (_req, res) => {
|
app.post(['/app/bitcoin-ui/bitcoin-rpc/', '/app/bitcoin-core/bitcoin-rpc/', '/app/bitcoin-knots/bitcoin-rpc/'], (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) => {
|
|
||||||
const { id = 'bitcoin-ui', method } = req.body || {}
|
const { id = 'bitcoin-ui', method } = req.body || {}
|
||||||
const results = {
|
const results = {
|
||||||
getblockchaininfo: mockBitcoinBlockchainInfo(),
|
getblockchaininfo: mockBitcoinBlockchainInfo(),
|
||||||
@ -1206,7 +1176,7 @@ app.post('/app/bitcoin-ui/bitcoin-rpc/', (req, res) => {
|
|||||||
res.json({ id, result: results[method], error: null })
|
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 { method, params = {} } = req.body || {}
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
switch (method) {
|
switch (method) {
|
||||||
@ -3781,6 +3751,29 @@ function demoNodeContext() {
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
app.post('/aiui/api/claude/*', async (req, res) => {
|
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
|
const apiKey = process.env.ANTHROPIC_API_KEY
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
|
|||||||
@ -53,15 +53,14 @@ export function clearDemoIntroSeen(): void {
|
|||||||
// external site). Everything else shows "No demo" on a disabled install button
|
// external site). Everything else shows "No demo" on a disabled install button
|
||||||
// and is not launchable.
|
// and is not launchable.
|
||||||
const DEMO_EXTERNAL_URLS: Record<string, string> = {
|
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: 'https://mempool.space/testnet',
|
||||||
'mempool-web': '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
|
// Apps with a same-origin mock UI served by the demo backend.
|
||||||
// served by the demo backend, or a header-stripping reverse-proxy (IndeeHub).
|
|
||||||
const DEMO_MOCK_UI: Record<string, string> = {
|
const DEMO_MOCK_UI: Record<string, string> = {
|
||||||
indeedhub: '/app/indeedhub/',
|
|
||||||
'bitcoin-knots': '/app/bitcoin-knots/',
|
'bitcoin-knots': '/app/bitcoin-knots/',
|
||||||
'bitcoin-core': '/app/bitcoin-core/',
|
'bitcoin-core': '/app/bitcoin-core/',
|
||||||
bitcoin: '/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
|
* Whether a demo app opens in a new tab. Nothing does — IndeeHub and Mempool
|
||||||
* cannot be iframed, so it opens in a new tab; IndeeHub still loads in-iframe.
|
* both load their real site directly in the in-app iframe.
|
||||||
*/
|
*/
|
||||||
export function isDemoExternal(appId: string): boolean {
|
export function isDemoExternal(_appId: string): boolean {
|
||||||
return appId === 'mempool' || appId === 'mempool-web'
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Can this app be launched/installed in the demo? */
|
/** Can this app be launched/installed in the demo? */
|
||||||
|
|||||||
@ -161,7 +161,6 @@ export default defineConfig({
|
|||||||
'/electrs-status': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false },
|
'/electrs-status': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false },
|
||||||
'/proxy': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false },
|
'/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 },
|
'/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/)
|
// Serve the node's deployed AIUI same-origin like production (set VITE_AIUI_URL=/aiui/)
|
||||||
'/aiui': {
|
'/aiui': {
|
||||||
target: process.env.AIUI_PROXY_TARGET || 'http://127.0.0.1:80',
|
target: process.env.AIUI_PROXY_TARGET || 'http://127.0.0.1:80',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user