feat(demo): black-theme app UIs w/ icons, real ElectrumX UI, Core/Knots split

- Mock app UIs (ElectrumX, LND, Fedimint, Bitcoin Core) + the "Not available"
  notice now use the Archipelago black theme and show the app's My-Apps icon.
- Bitcoin Core gets its own UI (/app/bitcoin-core/) so it no longer shows Bitcoin
  Knots branding; the Knots-branded bitcoin-ui shell is reserved for Bitcoin Knots.
- ElectrumX now serves the real electrs-ui shell (+ qrcode.js + a dummy
  /electrs-status) with the correct ElectrumX icon; "Electrs" renamed to ElectrumX.
- My Apps: pre-install Bitcoin Knots again, drop ThunderHub, rename Electrs→ElectrumX.
- App store no longer shows "Checking…" forever in demo — non-demoable apps show
  "No demo" immediately (skip the container-scan state).
- Relay endpoint no longer reveals a real domain (randomised host).
- Dockerfile/.dockerignore copy docker/electrs-ui into the backend image.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-06-22 13:55:50 -04:00
parent 4cc808c73e
commit a0f70b3949
8 changed files with 113 additions and 63 deletions

View File

@ -7,10 +7,11 @@
# Allow demo assets (AIUI pre-built dist)
!demo/
# Allow the Bitcoin UI mock shell (backend serves it from /docker/bitcoin-ui)
# Allow the Bitcoin UI + ElectrumX UI mock shells (served from /docker/*)
!docker/
docker/*
!docker/bitcoin-ui/
!docker/electrs-ui/
# Allow backend source for ISO source builds
!core/

View File

@ -17,6 +17,7 @@ COPY neode-ui/ ./
# Sibling assets the mock backend reads relative to /app (../docker, ../demo):
# the Bitcoin UI mock shell and any curated cloud files dropped into demo/files.
COPY docker/bitcoin-ui /docker/bitcoin-ui
COPY docker/electrs-ui /docker/electrs-ui
COPY demo/files /demo/files
# Expose port

View File

@ -62,6 +62,14 @@ http {
proxy_set_header X-Real-IP $remote_addr;
}
# ElectrumX UI status (polled by the electrs-ui shell)
location /electrs-status {
proxy_pass http://neode-backend:5959;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Proxy FileBrowser API to mock backend (demo mode)
location /app/filebrowser/ {
client_max_body_size 10G;

View File

@ -150,7 +150,7 @@ const SEED_BTCRELAY = {
allow_tor: true,
selected_peer_pubkey: '03d9b8a8db6b4f4d8b8d04c7a467c101f04c0ecbabc0e29e4dcb812a3b1c5f8f04',
http_endpoint: '',
https_endpoint: 'https://shard.tx1138.com/',
https_endpoint: 'https://relay-' + randomHex(5) + '.example.net/',
tor_endpoint: 'http://btc-relay-demoabcdefghijklmnop.onion/',
},
trusted_nodes: [
@ -758,19 +758,27 @@ function staticApp({ id, title, version, shortDesc, longDesc, license, state, la
}
// Static dev apps (always shown in My Apps when using mock backend).
// Only one Bitcoin implementation is pre-installed (Bitcoin Core); Bitcoin Knots
// remains installable from the marketplace.
const staticDevApps = {
bitcoin: staticApp({
id: 'bitcoin',
title: 'Bitcoin Core',
version: '27.1',
version: '28.4.0',
shortDesc: 'Full Bitcoin node',
longDesc: 'Validate every transaction and block. Full consensus enforcement — the bedrock of sovereignty.',
state: 'running',
lanPort: 8332,
icon: '/assets/img/app-icons/bitcoin-core.png',
}),
'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',
}),
lnd: staticApp({
id: 'lnd',
title: 'LND',
@ -779,15 +787,17 @@ const staticDevApps = {
longDesc: 'Instant Bitcoin payments with near-zero fees. Open channels, route payments, earn sats.',
state: 'running',
lanPort: 8080,
icon: '/assets/img/app-icons/lnd.svg',
}),
electrs: staticApp({
id: 'electrs',
title: 'Electrs',
version: '0.10.6',
shortDesc: 'Electrum Server in Rust',
electrumx: staticApp({
id: 'electrumx',
title: 'ElectrumX',
version: '1.18.0',
shortDesc: 'Electrum server',
longDesc: 'Private blockchain indexing for wallet lookups. Connect Sparrow, BlueWallet, or any Electrum-compatible wallet.',
state: 'running',
lanPort: 50001,
lanPort: 50002,
icon: '/assets/img/app-icons/electrumx.webp',
}),
mempool: staticApp({
id: 'mempool',
@ -829,15 +839,6 @@ const staticDevApps = {
lanPort: 8083,
icon: '/assets/img/app-icons/file-browser.webp',
}),
thunderhub: staticApp({
id: 'thunderhub',
title: 'ThunderHub',
version: '0.13.31',
shortDesc: 'Lightning node management UI',
longDesc: 'Full Lightning node management — channels, payments, routing fees, and node health. Connect to your LND and manage everything from one dashboard.',
state: 'running',
lanPort: 3010,
}),
fedimint: staticApp({
id: 'fedimint',
title: 'Fedimint',
@ -979,31 +980,38 @@ app.get('/app/bitcoin-ui/', async (_req, res) => {
})
// ── Mock app UIs served in the in-app iframe (DEMO) ─────────────────────────
function demoAppShell(title, accent, bodyHtml) {
function demoAppShell(title, sub, iconPath, bodyHtml) {
const accent = '#f7931a' // Archipelago orange
return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"><title>${title}</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0b0f1a;color:#e6e8ee;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-height:100vh;padding:28px}
body{background:#000;color:#f2f2f4;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-height:100vh;padding:28px;
background-image:radial-gradient(1200px 500px at 50% -10%, rgba(247,147,26,.07), transparent 70%);}
.wrap{max-width:860px;margin:0 auto}
.hd{display:flex;align-items:center;gap:14px;margin-bottom:22px}
.dot{width:11px;height:11px;border-radius:50%;background:${accent};box-shadow:0 0 12px ${accent}}
.ico{width:46px;height:46px;border-radius:12px;object-fit:cover;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);flex-shrink:0}
h1{font-size:22px;font-weight:650}
.sub{color:#8b93a7;font-size:13px;margin-top:2px}
.sub{color:#8a8f9a;font-size:13px;margin-top:2px}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px;margin-bottom:18px}
.card{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:16px}
.k{color:#8b93a7;font-size:12px;text-transform:uppercase;letter-spacing:.5px}
.card{background:rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:16px}
.k{color:#8a8f9a;font-size:12px;text-transform:uppercase;letter-spacing:.5px}
.v{font-size:20px;font-weight:650;margin-top:6px}
.v.mono{font-family:ui-monospace,Menlo,monospace;font-size:13px;word-break:break-all;font-weight:500}
.bar{height:8px;border-radius:6px;background:rgba(255,255,255,.08);overflow:hidden;margin-top:10px}
.bar{height:8px;border-radius:6px;background:rgba(255,255,255,.07);overflow:hidden;margin-top:10px}
.bar>i{display:block;height:100%;background:${accent}}
.badge{display:inline-flex;align-items:center;gap:6px;background:rgba(34,197,94,.15);color:#86efac;border:1px solid rgba(34,197,94,.3);padding:4px 10px;border-radius:999px;font-size:12px}
.badge{display:inline-flex;align-items:center;gap:6px;background:rgba(34,197,94,.14);color:#86efac;border:1px solid rgba(34,197,94,.3);padding:4px 10px;border-radius:999px;font-size:12px}
table{width:100%;border-collapse:collapse;font-size:13px}
td,th{text-align:left;padding:9px 6px;border-bottom:1px solid rgba(255,255,255,.06)}
th{color:#8b93a7;font-weight:500;font-size:11px;text-transform:uppercase}
.demo-tag{position:fixed;bottom:14px;right:16px;font-size:11px;color:#6b7280}
</style></head><body><div class="wrap">${bodyHtml}</div>
<div class="demo-tag">Archipelago demo · signet</div></body></html>`
th{color:#8a8f9a;font-weight:500;font-size:11px;text-transform:uppercase}
.demo-tag{position:fixed;bottom:14px;right:16px;font-size:11px;color:#5b6070}
</style></head><body><div class="wrap">
<div class="hd">
<img class="ico" src="${iconPath}" onerror="this.style.visibility='hidden'" alt="">
<div><h1>${title}</h1><div class="sub">${sub}</div></div>
</div>
${bodyHtml}
</div><div class="demo-tag">Archipelago demo · signet</div></body></html>`
}
// 12 trusted/federated nodes for the demo Federation view.
@ -1047,29 +1055,40 @@ function demoFederationNodes() {
})
}
app.get(['/app/electrumx/', '/app/electrs/', '/app/archy-electrs-ui/'], (_req, res) => {
res.type('html').send(demoAppShell('Electrs — Electrum Server', '#22d3ee', `
<div class="hd"><span class="dot"></span><div><h1>Electrs</h1><div class="sub">Electrum server in Rust · signet</div></div></div>
<div class="grid">
<div class="card"><div class="k">Status</div><div class="v"><span class="badge"> Serving clients</span></div></div>
<div class="card"><div class="k">Indexed height</div><div class="v">902,418</div><div class="bar"><i style="width:100%"></i></div></div>
<div class="card"><div class="k">RPC port</div><div class="v mono">50002 (SSL)</div></div>
<div class="card"><div class="k">Connected wallets</div><div class="v">3</div></div>
<div class="card"><div class="k">Mempool txs</div><div class="v">12,840</div></div>
<div class="card"><div class="k">DB size</div><div class="v">58.2 GB</div></div>
</div>
<div class="card"><div class="k" style="margin-bottom:8px">Recent client subscriptions</div>
<table><thead><tr><th>Wallet</th><th>Address type</th><th>Subscribed</th></tr></thead><tbody>
<tr><td>Sparrow</td><td>P2WPKH</td><td>2 min ago</td></tr>
<tr><td>BlueWallet</td><td>P2TR</td><td>9 min ago</td></tr>
<tr><td>Electrum</td><td>P2WPKH</td><td>21 min ago</td></tr>
</tbody></table>
</div>`))
// 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() }
})
// 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',
})
})
app.get(['/app/lnd/', '/app/lnd-ui/', '/app/archy-lnd-ui/', '/app/thunderhub/'], (_req, res) => {
res.type('html').send(demoAppShell('LND — Lightning', '#f7931a', `
<div class="hd"><span class="dot"></span><div><h1>LND</h1><div class="sub">Lightning Network Daemon · signet</div></div></div>
res.type('html').send(demoAppShell('LND', 'Lightning Network Daemon · signet', '/assets/img/app-icons/lnd.svg', `
<div class="grid">
<div class="card"><div class="k">Node</div><div class="v"><span class="badge"> Synced to chain</span></div></div>
<div class="card"><div class="k">Channel balance</div><div class="v">8,250,000 sat</div></div>
@ -1088,9 +1107,27 @@ app.get(['/app/lnd/', '/app/lnd-ui/', '/app/archy-lnd-ui/', '/app/thunderhub/'],
</div>`))
})
app.get(['/app/bitcoin-core/'], (_req, res) => {
res.type('html').send(demoAppShell('Bitcoin Core', 'Full node · signet', '/assets/img/app-icons/bitcoin-core.png', `
<div class="grid">
<div class="card"><div class="k">Status</div><div class="v"><span class="badge"> Synced</span></div></div>
<div class="card"><div class="k">Block height</div><div class="v">902,418</div><div class="bar"><i style="width:100%"></i></div></div>
<div class="card"><div class="k">Chain</div><div class="v">signet</div></div>
<div class="card"><div class="k">Connections</div><div class="v">18</div></div>
<div class="card"><div class="k">Mempool</div><div class="v">12,840 txs</div></div>
<div class="card"><div class="k">Version</div><div class="v mono">v28.4.0</div></div>
</div>
<div class="card"><div class="k" style="margin-bottom:8px">Peers</div>
<table><thead><tr><th>Address</th><th>Type</th><th>Height</th></tr></thead><tbody>
<tr><td>node1.signetonion</td><td>outbound</td><td>902,418</td></tr>
<tr><td>node2.signetonion</td><td>outbound</td><td>902,418</td></tr>
<tr><td>192.168.1.42</td><td>inbound</td><td>902,417</td></tr>
</tbody></table>
</div>`))
})
app.get(['/app/fedimint/', '/app/fedimintd/'], (_req, res) => {
res.type('html').send(demoAppShell('Fedimint — Guardian', '#a78bfa', `
<div class="hd"><span class="dot"></span><div><h1>Fedimint Guardian</h1><div class="sub">Archipelago Federation · 4-of-5 consensus</div></div></div>
res.type('html').send(demoAppShell('Fedimint Guardian', 'Archipelago Federation · 4-of-5 consensus', '/assets/img/app-icons/fedimint.png', `
<div class="grid">
<div class="card"><div class="k">Consensus</div><div class="v"><span class="badge"> Online</span></div></div>
<div class="card"><div class="k">Epoch</div><div class="v">48,217</div></div>
@ -4065,12 +4102,13 @@ app.get(['/app/:id', '/app/:id/'], (req, res) => {
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}</title>
<style>*{margin:0;padding:0;box-sizing:border-box}html,body{height:100%}
body{background:#0b0f1a;color:#e6e8ee;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;display:flex;align-items:center;justify-content:center}
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(900px 400px at 50% -10%, rgba(247,147,26,.07), transparent 70%)}
.card{text-align:center;padding:48px 40px;max-width:380px}
img{width:84px;height:84px;border-radius:20px;object-fit:cover;margin-bottom:20px;background:rgba(255,255,255,.05)}
img{width:84px;height:84px;border-radius:20px;object-fit:cover;margin-bottom:20px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08)}
h1{font-size:22px;font-weight:650;margin-bottom:8px}
p{color:#8b93a7;font-size:14px;line-height:1.5}
.tag{margin-top:18px;display:inline-block;font-size:12px;color:#fbbf24;border:1px solid rgba(251,191,36,.3);background:rgba(251,191,36,.1);padding:5px 12px;border-radius:999px}
p{color:#8a8f9a;font-size:14px;line-height:1.5}
.tag{margin-top:18px;display:inline-block;font-size:12px;color:#f7931a;border:1px solid rgba(247,147,26,.3);background:rgba(247,147,26,.1);padding:5px 12px;border-radius:999px}
</style></head><body><div class="card">
<img src="/assets/img/app-icons/${id}.png" onerror="this.onerror=null;this.src='/assets/icon/favico-black.svg'" alt="">
<h1>${title}</h1>

View File

@ -62,8 +62,8 @@ const DEMO_EXTERNAL_URLS: Record<string, string> = {
// Apps with a same-origin mock UI served by the demo backend.
const DEMO_MOCK_UI: Record<string, string> = {
'bitcoin-knots': '/app/bitcoin-ui/',
'bitcoin-core': '/app/bitcoin-ui/',
bitcoin: '/app/bitcoin-ui/',
'bitcoin-core': '/app/bitcoin-core/',
bitcoin: '/app/bitcoin-core/',
'bitcoin-ui': '/app/bitcoin-ui/',
electrs: '/app/electrumx/',
electrumx: '/app/electrumx/',

View File

@ -102,9 +102,9 @@
@click.stop="$emit('launch', app)"
class="px-4 py-2 glass-button glass-button-sm rounded-lg text-sm font-medium"
>Launch</button>
<!-- Scanning -->
<!-- Scanning (skipped in demo there are no real containers to scan) -->
<span
v-else-if="!containersScanned && (app.source === 'local' || app.dockerImage)"
v-else-if="!IS_DEMO && !containersScanned && (app.source === 'local' || app.dockerImage)"
class="flex-1 px-4 py-2 rounded-lg text-white/50 text-sm font-medium text-center cursor-default relative overflow-hidden"
>
<span class="discover-shimmer-bg"></span>

View File

@ -64,7 +64,7 @@
Starting...
</span>
<button
v-else-if="!containersScanned && app.dockerImage"
v-else-if="!IS_DEMO && !containersScanned && app.dockerImage"
disabled
class="text-white/40 text-sm flex items-center gap-2"
>

View File

@ -156,6 +156,8 @@ export default defineConfig({
'/app/electrs': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false },
'/app/lnd': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false },
'/app/fedimint': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false },
'/app/bitcoin-core': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false },
'/electrs-status': { target: process.env.BACKEND_URL || 'http://localhost:5959', changeOrigin: true, secure: false },
// 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',