frontend: polish app launch and release experience

This commit is contained in:
archipelago
2026-06-11 00:24:40 -04:00
parent c393b96da3
commit 1a3d726eac
140 changed files with 5930 additions and 920 deletions
+255
View File
@@ -103,6 +103,47 @@ const walletState = {
}
function randomHex(bytes) { return Array.from({length: bytes}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join('') }
const bitcoinRelayMockState = {
settings: {
enabled_for_peers: true,
allow_peer_requests: true,
allow_http: false,
allow_https: true,
allow_tor: true,
selected_peer_pubkey: '03d9b8a8db6b4f4d8b8d04c7a467c101f04c0ecbabc0e29e4dcb812a3b1c5f8f04',
http_endpoint: '',
https_endpoint: 'https://shard.tx1138.com/',
tor_endpoint: 'http://btc-relay-demoabcdefghijklmnop.onion/',
},
trusted_nodes: [
{
pubkey: '03d9b8a8db6b4f4d8b8d04c7a467c101f04c0ecbabc0e29e4dcb812a3b1c5f8f04',
onion: 'trustedalphaabcdefghijklmnop.onion',
name: 'Trusted Alpha',
relay_approved: true,
},
{
pubkey: '02f6ab6c88037cd527a92f3a016a7bd18bb2ebd91d5a3efb1161481b5cf7d9ea2a',
onion: 'trustedbetabcdefghijklmnopq.onion',
name: 'Trusted Beta',
relay_approved: false,
},
],
requests: [
{
id: 'relay-demo-incoming',
direction: 'incoming',
status: 'pending',
peer_pubkey: '02f6ab6c88037cd527a92f3a016a7bd18bb2ebd91d5a3efb1161481b5cf7d9ea2a',
peer_onion: 'trustedbetabcdefghijklmnopq.onion',
peer_name: 'Trusted Beta',
message: 'Can I use your node for a Wasabi broadcast?',
created_at: new Date(Date.now() - 12 * 60 * 1000).toISOString(),
updated_at: new Date(Date.now() - 12 * 60 * 1000).toISOString(),
},
],
}
// User state (simulated file-based storage)
let userState = {
setupComplete: false,
@@ -719,6 +760,16 @@ function staticApp({ id, title, version, shortDesc, longDesc, license, state, la
// Static dev apps (always shown in My Apps when using mock backend)
const staticDevApps = {
'bitcoin-knots': staticApp({
id: 'bitcoin-knots',
title: 'Bitcoin Knots',
version: '27.1',
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',
}),
bitcoin: staticApp({
id: 'bitcoin',
title: 'Bitcoin Core',
@@ -765,6 +816,16 @@ const staticDevApps = {
state: 'running',
lanPort: null,
}),
meshtastic: staticApp({
id: 'meshtastic',
title: 'Meshtastic',
version: '2-daily-alpine',
shortDesc: 'LoRa mesh networking',
longDesc: 'Open-source mesh networking for LoRa radios. Create decentralized communication networks.',
state: 'running',
lanPort: 4403,
icon: '/assets/img/app-icons/meshcore.svg',
}),
filebrowser: staticApp({
id: 'filebrowser',
title: 'File Browser',
@@ -826,6 +887,200 @@ app.options('/rpc/v1', (req, res) => {
res.status(200).end()
})
app.get('/', (_req, res) => {
const uiPort = process.env.VITE_DEV_SERVER_PORT || '8102'
res
.status(200)
.type('html')
.send(`<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=http://localhost:${uiPort}/">
<title>Archipelago dev backend</title>
</head>
<body>
<p>This is the mock JSON-RPC backend. Open the dashboard at
<a href="http://localhost:${uiPort}/">http://localhost:${uiPort}/</a>.
</p>
</body>
</html>`)
})
app.get('/rpc/v1', (_req, res) => {
const uiPort = process.env.VITE_DEV_SERVER_PORT || '8102'
res
.status(405)
.type('text/plain')
.send(`JSON-RPC is available at /rpc/v1 for POST requests only.\nOpen the dashboard at http://localhost:${uiPort}/.\n`)
})
function mockBitcoinBlockchainInfo() {
return {
chain: 'main',
blocks: 902418,
headers: 902418,
bestblockhash: randomHex(32),
difficulty: 126_984_812_384_099.3,
verificationprogress: 0.999998,
initialblockdownload: false,
size_on_disk: 678_000_000_000,
pruned: false,
chainwork: '00000000000000000000000000000000000000008d4b42d8b9b0000000000000',
}
}
function mockBitcoinNetworkInfo() {
return {
version: 270100,
subversion: '/Satoshi:27.1/Knots:20240513/',
protocolversion: 70016,
localservices: '0000000000000409',
localrelay: true,
timeoffset: 0,
networkactive: true,
connections: 18,
networks: [
{ name: 'ipv4', limited: false, reachable: true, proxy: '', proxy_randomize_credentials: false },
{ name: 'onion', limited: false, reachable: true, proxy: '127.0.0.1:9050', proxy_randomize_credentials: true },
],
}
}
function bitcoinRelayStatusPayload() {
return {
settings: bitcoinRelayMockState.settings,
trusted_nodes: bitcoinRelayMockState.trusted_nodes,
requests: bitcoinRelayMockState.requests,
local_node: {
synced: true,
blocks: 902418,
headers: 902418,
chain: 'main',
status_ok: true,
status_stale: false,
error: null,
},
credentials: {
username: 'txrelay',
available: true,
password_available: true,
rpcauth_available: true,
client_env_available: true,
client_env_path: '/var/lib/archipelago/secrets/bitcoin-rpc-txrelay-client.env',
restart_hint: 'If this was just generated, restart Bitcoin Core/Knots so bitcoind loads the txrelay rpcauth whitelist.',
},
}
}
app.get('/app/bitcoin-ui/', async (_req, res) => {
try {
const html = await fs.readFile(path.join(__dirname, '..', 'docker', 'bitcoin-ui', 'index.html'), 'utf8')
res.type('html').send(html)
} catch (error) {
res.status(500).type('text/plain').send(`Unable to load Bitcoin UI mock: ${error.message}`)
}
})
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) => {
const { id = 'bitcoin-ui', method } = req.body || {}
const results = {
getblockchaininfo: mockBitcoinBlockchainInfo(),
getnetworkinfo: mockBitcoinNetworkInfo(),
getindexinfo: {
txindex: { synced: true, best_block_height: 902418 },
blockfilterindex: { synced: true, best_block_height: 902418 },
},
getzmqnotifications: [
{ type: 'pubhashblock', address: 'tcp://127.0.0.1:28332', hwm: 1000 },
{ type: 'pubrawtx', address: 'tcp://127.0.0.1:28333', hwm: 1000 },
],
getmempoolinfo: { loaded: true, size: 18452, bytes: 62400000, usage: 143000000, maxmempool: 300000000 },
getpeerinfo: Array.from({ length: 18 }, (_, index) => ({ id: index, addr: `198.51.100.${index + 10}:8333`, inbound: index % 3 === 0 })),
}
if (!Object.prototype.hasOwnProperty.call(results, method)) {
return res.json({ id, result: null, error: { code: -32601, message: `Method not found: ${method}` } })
}
res.json({ id, result: results[method], error: null })
})
app.post('/app/bitcoin-ui/rpc/v1', (req, res) => {
const { method, params = {} } = req.body || {}
const now = new Date().toISOString()
switch (method) {
case 'bitcoin.relay-status':
return res.json({ result: bitcoinRelayStatusPayload(), error: null })
case 'bitcoin.relay-update-settings':
bitcoinRelayMockState.settings = {
...bitcoinRelayMockState.settings,
...params,
}
return res.json({ result: bitcoinRelayStatusPayload(), error: null })
case 'bitcoin.relay-request-peer': {
const peer = bitcoinRelayMockState.trusted_nodes.find(p => p.pubkey === params.peer_pubkey)
if (!peer) return res.status(400).json({ error: { message: 'Peer is not in trusted nodes' } })
const request = {
id: `relay-demo-${Date.now()}`,
direction: 'outbound',
status: 'pending',
peer_pubkey: peer.pubkey,
peer_onion: peer.onion,
peer_name: peer.name,
message: params.message || '',
created_at: now,
updated_at: now,
}
bitcoinRelayMockState.requests.push(request)
return res.json({ result: { ok: true, request_id: request.id }, error: null })
}
case 'bitcoin.relay-approve-request':
case 'bitcoin.relay-reject-request': {
const requestId = params.id || params.request_id
const request = bitcoinRelayMockState.requests.find(r => r.id === requestId)
if (!request) return res.status(404).json({ error: { message: `Request not found: ${requestId}` } })
request.status = method.endsWith('approve-request') ? 'approved' : 'rejected'
request.updated_at = now
if (request.status === 'approved') {
request.approved_endpoint = bitcoinRelayMockState.settings.https_endpoint || bitcoinRelayMockState.settings.tor_endpoint || bitcoinRelayMockState.settings.http_endpoint
request.credential_secret_path = '/var/lib/archipelago/secrets/bitcoin-relay-peer-demo.env'
}
return res.json({ result: { ok: true, request_id: request.id }, error: null })
}
case 'bitcoin.relay-create-tor-service':
bitcoinRelayMockState.settings.allow_tor = true
bitcoinRelayMockState.settings.tor_endpoint = 'http://btc-relay-demoabcdefghijklmnop.onion/'
return res.json({
result: {
created: true,
name: 'bitcoin-rpc',
onion_address: 'btc-relay-demoabcdefghijklmnop.onion',
},
error: null,
})
default:
return res.status(404).json({ error: { message: `Unknown mock Bitcoin UI RPC method: ${method}` } })
}
})
// RPC endpoint
app.post('/rpc/v1', (req, res) => {
const { method, params } = req.body