From 2cffa79d9dd5d6f70120e79f97de89e27011278d Mon Sep 17 00:00:00 2001 From: archipelago Date: Mon, 22 Jun 2026 10:26:35 -0400 Subject: [PATCH] feat(demo): app launch UIs, "No demo" gating, onboarding skip, 12 nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- neode-ui/mock-backend.js | 220 +++++++++++------- neode-ui/src/composables/useDemoIntro.ts | 40 ++++ neode-ui/src/stores/appLauncher.ts | 7 + neode-ui/src/views/Login.vue | 18 +- neode-ui/src/views/MarketplaceAppDetails.vue | 16 +- neode-ui/src/views/OnboardingIntro.vue | 49 ++-- .../src/views/appSession/appSessionConfig.ts | 7 + neode-ui/src/views/discover/AppGrid.vue | 7 + neode-ui/src/views/discover/FeaturedApps.vue | 6 + 9 files changed, 256 insertions(+), 114 deletions(-) diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index 26c91c69..ff5099dc 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -182,10 +182,11 @@ const SEED_BTCRELAY = { } // User state (simulated file-based storage). Returns a fresh object per session. -// In DEMO mode the effective dev mode is "onboarding" so the intro/onboarding can -// play for each new visitor (the per-day replay gate lives in the frontend). +// 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. function seedUserState() { - const mode = DEMO ? 'onboarding' : DEV_MODE + const mode = DEMO ? 'existing' : DEV_MODE switch (mode) { case 'setup': // Setup mode: user needs to set a password (simple setup, not onboarding). @@ -982,6 +983,117 @@ app.get('/app/bitcoin-ui/', async (_req, res) => { } }) +// ── Mock app UIs served in the in-app iframe (DEMO) ───────────────────────── +function demoAppShell(title, accent, bodyHtml) { + return ` +${title} +
${bodyHtml}
+
Archipelago demo · signet
` +} + +// 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', + }, + } + }) +} + +app.get(['/app/electrumx/', '/app/electrs/', '/app/archy-electrs-ui/'], (_req, res) => { + res.type('html').send(demoAppShell('Electrs — Electrum Server', '#22d3ee', ` +

Electrs

Electrum server in Rust · signet
+
+
Status
● Serving clients
+
Indexed height
902,418
+
RPC port
50002 (SSL)
+
Connected wallets
3
+
Mempool txs
12,840
+
DB size
58.2 GB
+
+
Recent client subscriptions
+ + + + +
WalletAddress typeSubscribed
SparrowP2WPKH2 min ago
BlueWalletP2TR9 min ago
ElectrumP2WPKH21 min ago
+
`)) +}) + +app.get(['/app/fedimint/', '/app/fedimintd/'], (_req, res) => { + res.type('html').send(demoAppShell('Fedimint — Guardian', '#a78bfa', ` +

Fedimint Guardian

Archipelago Federation · 4-of-5 consensus
+
+
Consensus
● Online
+
Epoch
48,217
+
Guardians online
5 / 5
+
E-cash issued
12,480,000 sat
+
Lightning gateway
● Connected
+
Block sync
902,418
+
+
Guardians
+ + + + + + +
GuardianPeerStatus
Guardian Alphapeer-0online
Guardian Betapeer-1online
Guardian Gammapeer-2online
Guardian Deltapeer-3online
Guardian Epsilonpeer-4online
+
`)) +}) + app.get('/app/bitcoin-ui/bitcoin-status', (_req, res) => { res.json({ ok: true, @@ -2003,84 +2115,7 @@ app.post('/rpc/v1', (req, res) => { // Federation (multi-node clusters) // ===================================================================== case 'federation.list-nodes': { - return res.json({ - result: { - nodes: [ - { - did: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9', - pubkey: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', - onion: 'peer1abc2def3ghi4jkl5mno6pqr7stu8vwx9yz.onion', - trust_level: 'trusted', - added_at: '2026-02-15T10:30:00Z', - name: 'archy-198', - last_seen: new Date(Date.now() - 120000).toISOString(), - last_state: { - timestamp: new Date(Date.now() - 120000).toISOString(), - apps: [ - { id: 'bitcoin-knots', status: 'running', version: '27.1' }, - { id: 'lnd', status: 'running', version: '0.18.0' }, - { id: 'mempool', status: 'running', version: '3.0' }, - { id: 'electrs', status: 'running', version: '0.10.0' }, - ], - cpu_usage_percent: 18.3, - mem_used_bytes: 6_200_000_000, - mem_total_bytes: 16_000_000_000, - disk_used_bytes: 820_000_000_000, - disk_total_bytes: 1_800_000_000_000, - uptime_secs: 604800, - tor_active: true, - }, - }, - { - did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', - pubkey: 'f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5', - onion: 'peer2xyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba.onion', - trust_level: 'trusted', - added_at: '2026-03-01T14:00:00Z', - name: 'arch-tailscale-1', - last_seen: new Date(Date.now() - 300000).toISOString(), - last_state: { - timestamp: new Date(Date.now() - 300000).toISOString(), - apps: [ - { id: 'bitcoin-knots', status: 'running', version: '27.1' }, - { id: 'lnd', status: 'running', version: '0.18.0' }, - { id: 'nextcloud', status: 'running', version: '29.0' }, - ], - cpu_usage_percent: 42.1, - mem_used_bytes: 10_500_000_000, - mem_total_bytes: 32_000_000_000, - disk_used_bytes: 1_200_000_000_000, - disk_total_bytes: 2_000_000_000_000, - uptime_secs: 259200, - tor_active: true, - }, - }, - { - did: 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb', - pubkey: 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', - onion: 'peer3mno6pqr7stu8vwx9yzabc2def3ghi4jkl5.onion', - trust_level: 'observer', - added_at: '2026-03-10T09:15:00Z', - name: 'bunker-alpha', - last_seen: new Date(Date.now() - 3600000).toISOString(), - last_state: { - timestamp: new Date(Date.now() - 3600000).toISOString(), - apps: [ - { id: 'bitcoin-knots', status: 'running', version: '27.1' }, - { id: 'vaultwarden', status: 'running', version: '1.31.0' }, - ], - cpu_usage_percent: 5.8, - mem_used_bytes: 2_100_000_000, - mem_total_bytes: 8_000_000_000, - disk_used_bytes: 450_000_000_000, - disk_total_bytes: 1_000_000_000_000, - uptime_secs: 1209600, - tor_active: false, - }, - }, - ], - }, - }) + return res.json({ result: { nodes: demoFederationNodes() } }) } case 'federation.invite': { @@ -3826,6 +3861,29 @@ app.get('/app/thunderhub/api/invoices', (req, res) => res.json(MOCK_LND_DATA.inv 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)) +// 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/[/] 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(` +${title} +
+ +

${title}

+

This app isn't interactive in the demo, but it runs fully on a real Archipelago node.

+
Demo preview
+
`) +}) + // Health check app.get('/health', (req, res) => { res.status(200).send('healthy') diff --git a/neode-ui/src/composables/useDemoIntro.ts b/neode-ui/src/composables/useDemoIntro.ts index a05b15c9..c3942726 100644 --- a/neode-ui/src/composables/useDemoIntro.ts +++ b/neode-ui/src/composables/useDemoIntro.ts @@ -47,3 +47,43 @@ export function clearDemoIntroSeen(): void { /* ignore */ } } + +// ── Demoable apps ─────────────────────────────────────────────────────────── +// Only these apps actually do something in the demo (a mock UI or a real +// external site). Everything else shows "No demo" on a disabled install button +// and is not launchable. +const DEMO_EXTERNAL_URLS: Record = { + // Real, public sites that we open instead of mocking. + indeedhub: 'https://indee.tx1138.com/', + mempool: 'https://mempool.space/testnet', + 'mempool-web': 'https://mempool.space/testnet', +} + +// Apps with a same-origin mock UI served by the demo backend. +const DEMO_MOCK_UI: Record = { + 'bitcoin-knots': '/app/bitcoin-ui/', + 'bitcoin-core': '/app/bitcoin-ui/', + bitcoin: '/app/bitcoin-ui/', + 'bitcoin-ui': '/app/bitcoin-ui/', + electrs: '/app/electrumx/', + electrumx: '/app/electrumx/', + 'archy-electrs-ui': '/app/electrumx/', + fedimint: '/app/fedimint/', + fedimintd: '/app/fedimint/', + filebrowser: '/app/filebrowser/', +} + +/** Apps that open in a new tab (external real sites) rather than an iframe. */ +export function isDemoExternal(appId: string): boolean { + return appId === 'mempool' || appId === 'mempool-web' +} + +/** Can this app be launched/installed in the demo? */ +export function isDemoApp(appId: string): boolean { + return appId in DEMO_EXTERNAL_URLS || appId in DEMO_MOCK_UI +} + +/** Resolve the demo launch URL for an app, or null if it isn't demoable. */ +export function demoAppUrl(appId: string): string | null { + return DEMO_EXTERNAL_URLS[appId] ?? DEMO_MOCK_UI[appId] ?? null +} diff --git a/neode-ui/src/stores/appLauncher.ts b/neode-ui/src/stores/appLauncher.ts index 68bc66f1..4c7206ef 100644 --- a/neode-ui/src/stores/appLauncher.ts +++ b/neode-ui/src/stores/appLauncher.ts @@ -4,6 +4,7 @@ import { rpcClient } from '@/api/rpc-client' import router from '@/router' import { recordAppLaunch } from '@/utils/appUsage' import { requestExternalOpen } from '@/api/remote-relay' +import { IS_DEMO, isDemoExternal, demoAppUrl } from '@/composables/useDemoIntro' /** * Open a URL in a new browser tab — but if a companion (phone) is currently @@ -222,6 +223,12 @@ export const useAppLauncherStore = defineStore('appLauncher', () => { function openSession(appId: string) { recordAppLaunch(appId) const mobile = isMobileViewport() + // Demo: apps backed by a real external site that blocks iframing (mempool.space) + // open in a new tab; everything else demoable renders in the in-app session. + if (IS_DEMO && isDemoExternal(appId)) { + const ext = demoAppUrl(appId) + if (ext) { openExternal(ext); return } + } const launchUrl = NEW_TAB_APP_IDS.has(appId) ? directAppUrl(appId) : null if (launchUrl && !mobile) { openExternal(launchUrl) diff --git a/neode-ui/src/views/Login.vue b/neode-ui/src/views/Login.vue index bbc477e4..63aff776 100644 --- a/neode-ui/src/views/Login.vue +++ b/neode-ui/src/views/Login.vue @@ -208,14 +208,16 @@ > {{ t('login.replayIntro') }} - | - + diff --git a/neode-ui/src/views/MarketplaceAppDetails.vue b/neode-ui/src/views/MarketplaceAppDetails.vue index b6d13954..f4ae28ed 100644 --- a/neode-ui/src/views/MarketplaceAppDetails.vue +++ b/neode-ui/src/views/MarketplaceAppDetails.vue @@ -63,8 +63,8 @@ @@ -129,8 +129,8 @@ @@ -351,6 +351,7 @@