|
|
|
@ -150,7 +150,7 @@ const SEED_BTCRELAY = {
|
|
|
|
allow_tor: true,
|
|
|
|
allow_tor: true,
|
|
|
|
selected_peer_pubkey: '03d9b8a8db6b4f4d8b8d04c7a467c101f04c0ecbabc0e29e4dcb812a3b1c5f8f04',
|
|
|
|
selected_peer_pubkey: '03d9b8a8db6b4f4d8b8d04c7a467c101f04c0ecbabc0e29e4dcb812a3b1c5f8f04',
|
|
|
|
http_endpoint: '',
|
|
|
|
http_endpoint: '',
|
|
|
|
https_endpoint: 'https://shard.tx1138.com/',
|
|
|
|
https_endpoint: 'https://relay-' + randomHex(5) + '.example.net/',
|
|
|
|
tor_endpoint: 'http://btc-relay-demoabcdefghijklmnop.onion/',
|
|
|
|
tor_endpoint: 'http://btc-relay-demoabcdefghijklmnop.onion/',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
trusted_nodes: [
|
|
|
|
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).
|
|
|
|
// 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 = {
|
|
|
|
const staticDevApps = {
|
|
|
|
bitcoin: staticApp({
|
|
|
|
bitcoin: staticApp({
|
|
|
|
id: 'bitcoin',
|
|
|
|
id: 'bitcoin',
|
|
|
|
title: 'Bitcoin Core',
|
|
|
|
title: 'Bitcoin Core',
|
|
|
|
version: '27.1',
|
|
|
|
version: '28.4.0',
|
|
|
|
shortDesc: 'Full Bitcoin node',
|
|
|
|
shortDesc: 'Full Bitcoin node',
|
|
|
|
longDesc: 'Validate every transaction and block. Full consensus enforcement — the bedrock of sovereignty.',
|
|
|
|
longDesc: 'Validate every transaction and block. Full consensus enforcement — the bedrock of sovereignty.',
|
|
|
|
state: 'running',
|
|
|
|
state: 'running',
|
|
|
|
lanPort: 8332,
|
|
|
|
lanPort: 8332,
|
|
|
|
icon: '/assets/img/app-icons/bitcoin-core.png',
|
|
|
|
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({
|
|
|
|
lnd: staticApp({
|
|
|
|
id: 'lnd',
|
|
|
|
id: 'lnd',
|
|
|
|
title: 'LND',
|
|
|
|
title: 'LND',
|
|
|
|
@ -779,15 +787,17 @@ const staticDevApps = {
|
|
|
|
longDesc: 'Instant Bitcoin payments with near-zero fees. Open channels, route payments, earn sats.',
|
|
|
|
longDesc: 'Instant Bitcoin payments with near-zero fees. Open channels, route payments, earn sats.',
|
|
|
|
state: 'running',
|
|
|
|
state: 'running',
|
|
|
|
lanPort: 8080,
|
|
|
|
lanPort: 8080,
|
|
|
|
|
|
|
|
icon: '/assets/img/app-icons/lnd.svg',
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
electrs: staticApp({
|
|
|
|
electrumx: staticApp({
|
|
|
|
id: 'electrs',
|
|
|
|
id: 'electrumx',
|
|
|
|
title: 'Electrs',
|
|
|
|
title: 'ElectrumX',
|
|
|
|
version: '0.10.6',
|
|
|
|
version: '1.18.0',
|
|
|
|
shortDesc: 'Electrum Server in Rust',
|
|
|
|
shortDesc: 'Electrum server',
|
|
|
|
longDesc: 'Private blockchain indexing for wallet lookups. Connect Sparrow, BlueWallet, or any Electrum-compatible wallet.',
|
|
|
|
longDesc: 'Private blockchain indexing for wallet lookups. Connect Sparrow, BlueWallet, or any Electrum-compatible wallet.',
|
|
|
|
state: 'running',
|
|
|
|
state: 'running',
|
|
|
|
lanPort: 50001,
|
|
|
|
lanPort: 50002,
|
|
|
|
|
|
|
|
icon: '/assets/img/app-icons/electrumx.webp',
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
mempool: staticApp({
|
|
|
|
mempool: staticApp({
|
|
|
|
id: 'mempool',
|
|
|
|
id: 'mempool',
|
|
|
|
@ -829,15 +839,6 @@ const staticDevApps = {
|
|
|
|
lanPort: 8083,
|
|
|
|
lanPort: 8083,
|
|
|
|
icon: '/assets/img/app-icons/file-browser.webp',
|
|
|
|
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({
|
|
|
|
fedimint: staticApp({
|
|
|
|
id: 'fedimint',
|
|
|
|
id: 'fedimint',
|
|
|
|
title: '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) ─────────────────────────
|
|
|
|
// ── 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">
|
|
|
|
return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
|
|
|
|
<meta name="viewport" content="width=device-width,initial-scale=1"><title>${title}</title>
|
|
|
|
<meta name="viewport" content="width=device-width,initial-scale=1"><title>${title}</title>
|
|
|
|
<style>
|
|
|
|
<style>
|
|
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
|
|
*{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}
|
|
|
|
.wrap{max-width:860px;margin:0 auto}
|
|
|
|
.hd{display:flex;align-items:center;gap:14px;margin-bottom:22px}
|
|
|
|
.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}
|
|
|
|
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}
|
|
|
|
.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}
|
|
|
|
.card{background:rgba(255,255,255,.035);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}
|
|
|
|
.k{color:#8a8f9a;font-size:12px;text-transform:uppercase;letter-spacing:.5px}
|
|
|
|
.v{font-size:20px;font-weight:650;margin-top:6px}
|
|
|
|
.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}
|
|
|
|
.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}}
|
|
|
|
.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}
|
|
|
|
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)}
|
|
|
|
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}
|
|
|
|
th{color:#8a8f9a;font-weight:500;font-size:11px;text-transform:uppercase}
|
|
|
|
.demo-tag{position:fixed;bottom:14px;right:16px;font-size:11px;color:#6b7280}
|
|
|
|
.demo-tag{position:fixed;bottom:14px;right:16px;font-size:11px;color:#5b6070}
|
|
|
|
</style></head><body><div class="wrap">${bodyHtml}</div>
|
|
|
|
</style></head><body><div class="wrap">
|
|
|
|
<div class="demo-tag">Archipelago demo · signet</div></body></html>`
|
|
|
|
<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.
|
|
|
|
// 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) => {
|
|
|
|
// ElectrumX: serve the real electrs-ui shell (its qrcode.js sibling + the
|
|
|
|
res.type('html').send(demoAppShell('Electrs — Electrum Server', '#22d3ee', `
|
|
|
|
// /electrs-status endpoint it polls are served below with dummy data).
|
|
|
|
<div class="hd"><span class="dot"></span><div><h1>Electrs</h1><div class="sub">Electrum server in Rust · signet</div></div></div>
|
|
|
|
app.get(['/app/electrumx/', '/app/electrs/', '/app/archy-electrs-ui/'], async (_req, res) => {
|
|
|
|
<div class="grid">
|
|
|
|
try {
|
|
|
|
<div class="card"><div class="k">Status</div><div class="v"><span class="badge">● Serving clients</span></div></div>
|
|
|
|
const html = await fs.readFile(path.join(__dirname, '..', 'docker', 'electrs-ui', 'index.html'), 'utf8')
|
|
|
|
<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>
|
|
|
|
res.type('html').send(html)
|
|
|
|
<div class="card"><div class="k">RPC port</div><div class="v mono">50002 (SSL)</div></div>
|
|
|
|
} catch (error) {
|
|
|
|
<div class="card"><div class="k">Connected wallets</div><div class="v">3</div></div>
|
|
|
|
res.status(500).type('text/plain').send(`Unable to load ElectrumX UI mock: ${error.message}`)
|
|
|
|
<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>
|
|
|
|
app.get(['/app/electrumx/qrcode.js', '/app/electrs/qrcode.js', '/app/archy-electrs-ui/qrcode.js'], async (_req, res) => {
|
|
|
|
<div class="card"><div class="k" style="margin-bottom:8px">Recent client subscriptions</div>
|
|
|
|
try {
|
|
|
|
<table><thead><tr><th>Wallet</th><th>Address type</th><th>Subscribed</th></tr></thead><tbody>
|
|
|
|
const js = await fs.readFile(path.join(__dirname, '..', 'docker', 'electrs-ui', 'qrcode.js'), 'utf8')
|
|
|
|
<tr><td>Sparrow</td><td>P2WPKH</td><td>2 min ago</td></tr>
|
|
|
|
res.type('application/javascript').send(js)
|
|
|
|
<tr><td>BlueWallet</td><td>P2TR</td><td>9 min ago</td></tr>
|
|
|
|
} catch { res.status(404).end() }
|
|
|
|
<tr><td>Electrum</td><td>P2WPKH</td><td>21 min ago</td></tr>
|
|
|
|
})
|
|
|
|
</tbody></table>
|
|
|
|
// Dummy status for both the electrs-ui shell and the in-app ElectrumX sync screen.
|
|
|
|
</div>`))
|
|
|
|
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) => {
|
|
|
|
app.get(['/app/lnd/', '/app/lnd-ui/', '/app/archy-lnd-ui/', '/app/thunderhub/'], (_req, res) => {
|
|
|
|
res.type('html').send(demoAppShell('LND — Lightning', '#f7931a', `
|
|
|
|
res.type('html').send(demoAppShell('LND', 'Lightning Network Daemon · signet', '/assets/img/app-icons/lnd.svg', `
|
|
|
|
<div class="hd"><span class="dot"></span><div><h1>LND</h1><div class="sub">Lightning Network Daemon · signet</div></div></div>
|
|
|
|
|
|
|
|
<div class="grid">
|
|
|
|
<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">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>
|
|
|
|
<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>`))
|
|
|
|
</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.signet…onion</td><td>outbound</td><td>902,418</td></tr>
|
|
|
|
|
|
|
|
<tr><td>node2.signet…onion</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) => {
|
|
|
|
app.get(['/app/fedimint/', '/app/fedimintd/'], (_req, res) => {
|
|
|
|
res.type('html').send(demoAppShell('Fedimint — Guardian', '#a78bfa', `
|
|
|
|
res.type('html').send(demoAppShell('Fedimint Guardian', 'Archipelago Federation · 4-of-5 consensus', '/assets/img/app-icons/fedimint.png', `
|
|
|
|
<div class="hd"><span class="dot"></span><div><h1>Fedimint Guardian</h1><div class="sub">Archipelago Federation · 4-of-5 consensus</div></div></div>
|
|
|
|
|
|
|
|
<div class="grid">
|
|
|
|
<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">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>
|
|
|
|
<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">
|
|
|
|
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>
|
|
|
|
<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%}
|
|
|
|
<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}
|
|
|
|
.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}
|
|
|
|
h1{font-size:22px;font-weight:650;margin-bottom:8px}
|
|
|
|
p{color:#8b93a7;font-size:14px;line-height:1.5}
|
|
|
|
p{color:#8a8f9a;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}
|
|
|
|
.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">
|
|
|
|
</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="">
|
|
|
|
<img src="/assets/img/app-icons/${id}.png" onerror="this.onerror=null;this.src='/assets/icon/favico-black.svg'" alt="">
|
|
|
|
<h1>${title}</h1>
|
|
|
|
<h1>${title}</h1>
|
|
|
|
|