2026-03-19 17:02:17 +00:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< meta http-equiv = "Cache-Control" content = "no-cache, no-store, must-revalidate" >
< title > LND - Archipelago< / title >
< style >
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-height: 100vh; color: white; overflow-x: hidden; }
.bg-layer { position: fixed; inset: 0; z-index: -10; background: linear-gradient(135deg, rgba(0,0,0,0.9) 0%, rgba(30,30,50,0.95) 100%); }
.overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.8); z-index: -5; }
.glass-card { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border-radius: 1rem; border: 1px solid rgba(255, 255, 255, 0.18); }
.info-card { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(24px); border-radius: 16px; padding: 12px; border: 1px solid rgba(255, 255, 255, 0.1); }
.info-card-button { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(24px); border-radius: 16px; padding: 12px; border: 1px solid rgba(255, 255, 255, 0.1); cursor: pointer; color: rgba(255,255,255,0.9); transition: all 0.3s ease; }
.info-card-button:hover { transform: translateY(-2px); background: rgba(255,255,255,0.08); }
.info-card-button:active { transform: translateY(1px); }
.glass-button { background-color: rgba(0, 0, 0, 0.6); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); border: 1px solid rgba(255, 255, 255, 0.18); color: rgba(255, 255, 255, 0.9); transition: all 0.3s ease; cursor: pointer; }
.glass-button:hover { color: white; background-color: rgba(0, 0, 0, 0.7); }
.glass-button:disabled { opacity: 0.5; cursor: not-allowed; }
.container { max-width: 56rem; margin: 0 auto; padding: 2rem; padding-bottom: 4rem; }
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.items-start { align-items: start; }
.justify-between { justify-content: space-between; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
.gap-6 { gap: 1.5rem; }
.flex-1 { flex: 1; }
.flex-shrink-0 { flex-shrink: 0; }
.min-w-0 { min-width: 0; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.p-2 { padding: 0.5rem; }
.p-3 { padding: 0.75rem; }
.p-4 { padding: 1rem; }
.p-6 { padding: 1.5rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-1-5 { padding-top: 0.375rem; padding-bottom: 0.375rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-2-5 { padding-top: 0.625rem; padding-bottom: 0.625rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.grid { display: grid; }
.grid-cols-1 { grid-template-columns: repeat(1, 1fr); }
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
.rounded-lg { border-radius: 0.5rem; }
.rounded-xl { border-radius: 0.75rem; }
.rounded-full { border-radius: 9999px; }
.overflow-hidden { overflow: hidden; }
.transition-all { transition: all 0.3s ease; }
.text-xs { font-size: 0.75rem; }
.text-sm { font-size: 0.875rem; }
.text-lg { font-size: 1.125rem; }
.text-xl { font-size: 1.25rem; }
.text-2xl { font-size: 1.5rem; }
.text-3xl { font-size: 1.875rem; }
.font-bold { font-weight: 700; }
.font-semibold { font-weight: 600; }
.font-medium { font-weight: 500; }
.font-mono { font-family: monospace; }
.uppercase { text-transform: uppercase; }
.tracking-wide { letter-spacing: 0.05em; }
.truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.w-full { width: 100%; }
.text-white { color: white; }
.text-white-90 { color: rgba(255,255,255,0.9); }
.text-white-80 { color: rgba(255,255,255,0.8); }
.text-white-70 { color: rgba(255,255,255,0.7); }
.text-white-60 { color: rgba(255,255,255,0.6); }
.text-white-50 { color: rgba(255,255,255,0.5); }
.text-white-45 { color: rgba(255,255,255,0.45); }
.text-white-40 { color: rgba(255,255,255,0.4); }
.text-green { color: #4ade80; }
.text-orange { color: #fb923c; }
.text-red { color: #f87171; }
.bg-green { background: #4ade80; }
.bg-yellow { background: #facc15; }
.bg-red { background: #f87171; }
.icon-box { width: 4rem; height: 4rem; border-radius: 0.5rem; background: rgba(139, 92, 246, 0.2); display: flex; align-items: center; justify-content: center; }
.icon-box-sm { width: 3rem; height: 3rem; border-radius: 0.5rem; background: rgba(255,255,255,0.1); display: flex; align-items: center; justify-content: center; }
.status-dot { width: 0.75rem; height: 0.75rem; border-radius: 9999px; }
.status-dot-sm { width: 0.625rem; height: 0.625rem; border-radius: 9999px; display: inline-block; }
.relative { position: relative; }
.version-text { font-size: 0.75rem; color: rgba(255,255,255,0.4); }
/* Logo border matching electrs icon-box but with LND purple accent */
.logo-border { width: 4rem; height: 4rem; border-radius: 0.5rem; background: rgba(139, 92, 246, 0.2); display: flex; align-items: center; justify-content: center; overflow: hidden; }
.logo-border img { width: 3rem; height: 3rem; object-fit: contain; }
/* Stat row inside info-card */
.stat-row { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem; background: rgba(255,255,255,0.05); border-radius: 0.5rem; }
/* Balance cards */
.balance-card { padding: 1rem; background: rgba(0,0,0,0.3); border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.08); }
/* Ping animation */
@keyframes ping { 75%, 100% { transform: scale(2); opacity: 0; } }
.animate-ping { animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; position: absolute; inset: 0; }
/* Connection details */
.conn-select { width: 100%; padding: 0.75rem 1rem; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 0.5rem; color: white; font-size: 0.875rem; font-weight: 500; appearance: none; cursor: pointer; outline: none; background-image: url('data:image/svg+xml;utf8,< svg fill = "white" viewBox = "0 0 24 24" xmlns = "http://www.w3.org/2000/svg" > < path d = "M7 10l5 5 5-5z" / > < / svg > '); background-repeat: no-repeat; background-position: right 12px center; background-size: 20px; }
.conn-select:focus { border-color: rgba(255,255,255,0.25); }
.conn-select option { background: #1a1a2e; color: white; }
.conn-layout { display: flex; flex-direction: column; gap: 1.5rem; }
@media (min-width: 640px) { .conn-layout { flex-direction: row; } }
.qr-box { flex-shrink: 0; width: 196px; height: 196px; background: white; border-radius: 0.75rem; padding: 0.5rem; display: flex; align-items: center; justify-content: center; }
.qr-box img { width: 100%; height: auto; display: block; image-rendering: pixelated; }
.conn-fields { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.75rem; }
.field-label { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.45); margin-bottom: 0.25rem; }
.field-row { display: flex; align-items: center; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 0.5rem; overflow: hidden; min-width: 0; }
.field-value { flex: 1; min-width: 0; padding: 0.625rem 0.875rem; font-family: monospace; font-size: 0.8125rem; color: rgba(255,255,255,0.9); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.copy-btn { padding: 0.625rem 0.75rem; background: none; border: none; border-left: 1px solid rgba(255,255,255,0.1); cursor: pointer; color: rgba(255,255,255,0.4); transition: all 0.2s ease; display: flex; align-items: center; }
.copy-btn:hover { color: rgba(255,255,255,0.8); background: rgba(255,255,255,0.05); }
.copy-btn.copied { color: #4ade80; }
.field-row-split { display: flex; gap: 0.75rem; }
.field-row-split > div { flex: 1; }
.help-text { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid rgba(255,255,255,0.08); font-size: 0.875rem; color: rgba(255,255,255,0.5); line-height: 1.5; }
/* Modal */
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.8); backdrop-filter: blur(4px); z-index: 50; align-items: center; justify-content: center; padding: 1rem; }
.modal-overlay.visible { display: flex; }
.modal-content { max-width: 42rem; width: 100%; max-height: 85vh; overflow: hidden; display: flex; flex-direction: column; }
.modal-body { overflow-y: auto; flex: 1; min-height: 0; }
.modal-tab { flex: 1; padding: 0.5rem 1rem; text-align: center; border-radius: 0.5rem; font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; color: rgba(255,255,255,0.6); background: none; border: none; }
.modal-tab.active { background: rgba(255,255,255,0.2); color: white; }
.modal-tab:not(.active):hover { color: rgba(255,255,255,0.9); }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
.logs-box { background: rgba(0,0,0,0.4); border-radius: 0.5rem; padding: 1rem; font-family: monospace; font-size: 0.75rem; color: rgba(255,255,255,0.8); white-space: pre-wrap; word-break: break-all; min-height: 200px; }
/* Responsive grid */
@media (min-width: 768px) {
.md-grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.md-grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
.md-grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
.md-flex-row { flex-direction: row; }
}
< / style >
< / head >
< body >
< div class = "bg-layer" > < / div >
< div class = "overlay" > < / div >
< div class = "container" >
<!-- Header -->
< div class = "glass-card p-6 mb-6" >
< div class = "flex flex-col md-flex-row items-center gap-4" >
< div class = "logo-border flex-shrink-0" >
< img src = "assets/img/app-icons/lnd.svg" alt = "LND" / >
< / div >
< div class = "flex-1 min-w-0" >
< div class = "flex items-center gap-3" >
< h1 class = "text-2xl font-bold text-white" > LND< / h1 >
< / div >
< p class = "text-white-70" > Lightning Network Daemon for instant Bitcoin payments< / p >
< p class = "text-sm text-white-60 mt-2" id = "headerNetwork" > — < / p >
< / div >
< button onclick = "openSettings()" class = "glass-button flex items-center gap-2 px-4 py-2-5 rounded-lg text-sm font-medium flex-shrink-0" >
< svg style = "width:1.25rem;height:1.25rem" fill = "none" stroke = "currentColor" stroke-width = "2" viewBox = "0 0 24 24" >
< path stroke-linecap = "round" stroke-linejoin = "round" d = "M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" / >
< path stroke-linecap = "round" stroke-linejoin = "round" d = "M15 12a3 3 0 11-6 0 3 3 0 016 0z" / >
< / svg >
Settings
< / button >
< / div >
< / div >
<!-- Summary strip -->
< div class = "glass-card p-6 mb-6" >
< div class = "grid grid-cols-2 md-grid-cols-4 gap-3" >
< div class = "info-card flex items-center gap-3" >
< div class = "relative" style = "width:0.75rem;height:0.75rem" >
< div class = "status-dot bg-green" id = "statusDot" > < / div >
< div class = "status-dot bg-green animate-ping" id = "statusPing" style = "display:none;opacity:0.75" > < / div >
< / div >
< div >
< p class = "text-xs text-white-60 mb-1" > Node Status< / p >
< p class = "text-sm font-medium text-white" id = "summaryNodeStatus" > — < / p >
< / div >
< / div >
< div class = "info-card flex items-center gap-3" >
< span style = "font-size:1.5rem;color:#fb923c;font-weight:700" > ⚡ < / span >
< div >
< p class = "text-xs text-white-60 mb-1" > Channels< / p >
< p class = "text-sm font-medium text-orange" id = "channelCount" > 0< / p >
< / div >
< / div >
< div class = "info-card flex items-center justify-between" >
< div class = "flex items-center gap-3" >
< div class = "status-dot-sm bg-green" id = "restDot" > < / div >
< div >
< p class = "text-xs text-white-60 mb-1" > REST API< / p >
< p class = "text-sm font-medium text-white" id = "summaryRestStatus" > — < / p >
< / div >
< / div >
< button onclick = "openSettings(); setSettingsTab('rest');" class = "glass-button px-3 py-1-5 rounded-lg text-xs font-medium" > Settings< / button >
< / div >
< div class = "info-card flex items-center justify-between" >
< div class = "flex items-center gap-3" >
< div class = "status-dot-sm bg-green" id = "grpcDot" > < / div >
< div >
< p class = "text-xs text-white-60 mb-1" > gRPC< / p >
< p class = "text-sm font-medium text-white" id = "summaryGrpcStatus" > — < / p >
< / div >
< / div >
< button onclick = "openSettings(); setSettingsTab('logs');" class = "glass-button px-3 py-1-5 rounded-lg text-xs font-medium" > Logs< / button >
< / div >
< / div >
< / div >
<!-- Wallet -->
< div class = "glass-card p-6 mb-8" >
< div class = "flex items-start gap-4 mb-4" >
< div class = "icon-box-sm flex-shrink-0" >
< svg style = "width:1.5rem;height:1.5rem;color:#fb923c" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" / >
< / svg >
< / div >
< div class = "flex-1" >
< h2 class = "text-xl font-semibold text-white mb-1" > Wallet< / h2 >
< p class = "text-white-60 text-sm" > Balance, Receive, and Send will appear here when connected to your node.< / p >
< / div >
< / div >
< div class = "grid grid-cols-1 md-grid-cols-3 gap-3 mb-4" >
< div class = "balance-card" >
< p class = "text-xs text-white-50 uppercase tracking-wide mb-1" > Spendable< / p >
< p class = "text-lg font-semibold text-white" id = "balanceSpendable" > — < / p >
< / div >
< div class = "balance-card" >
< p class = "text-xs text-white-50 uppercase tracking-wide mb-1" > Lightning< / p >
< p class = "text-lg font-semibold text-white" id = "balanceLightning" > — < / p >
< / div >
< div class = "balance-card" >
< p class = "text-xs text-white-50 uppercase tracking-wide mb-1" > Total< / p >
< p class = "text-lg font-semibold text-white" id = "balanceTotal" > — < / p >
< / div >
< / div >
< div class = "flex gap-4" >
< button class = "glass-button px-6 py-3 rounded-lg font-medium" disabled > Receive< / button >
< button class = "glass-button px-6 py-3 rounded-lg font-medium" disabled > Send< / button >
< / div >
< p class = "text-white-50 text-xs mt-4" > Recent activity will be listed here.< / p >
< / div >
<!-- Connect Your Wallet -->
< div class = "glass-card p-6 mb-8" >
< h2 class = "text-xl font-semibold text-white mb-1" > Connect Your Wallet< / h2 >
< p class = "text-white-70 text-sm mb-4" id = "connSubtitle" > Use a wallet like Zeus, Zap, or BlueWallet to connect remotely.< / p >
<!-- Mode selector -->
< div class = "mb-4" >
< select id = "connMode" onchange = "updateConnInfo()" class = "conn-select" >
< option value = "rest-tor" > REST (Tor)< / option >
< option value = "rest-local" > REST (Local Network)< / option >
< option value = "grpc-tor" > gRPC (Tor)< / option >
< option value = "grpc-local" > gRPC (Local Network)< / option >
< / select >
< / div >
<!-- Connection display -->
< div class = "conn-layout" id = "connDisplay" >
< div class = "qr-box" id = "lndQrBox" >
< div style = "color:#999;font-size:12px;text-align:center;padding:2rem" > Loading...< / div >
< / div >
< div class = "conn-fields" >
< div >
< div class = "field-label" > Host< / div >
< div class = "field-row" >
< span class = "field-value" id = "connHost" > — < / span >
< button class = "copy-btn" onclick = "copyEl('connHost', this)" title = "Copy" >
< svg width = "16" height = "16" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < rect x = "9" y = "9" width = "13" height = "13" rx = "2" ry = "2" stroke-width = "2" / > < path d = "M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width = "2" / > < / svg >
< / button >
< / div >
< / div >
< div class = "field-row-split" >
< div >
< div class = "field-label" > Port< / div >
< div class = "field-row" >
< span class = "field-value" id = "connPort" > — < / span >
< button class = "copy-btn" onclick = "copyEl('connPort', this)" title = "Copy" >
< svg width = "16" height = "16" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < rect x = "9" y = "9" width = "13" height = "13" rx = "2" ry = "2" stroke-width = "2" / > < path d = "M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width = "2" / > < / svg >
< / button >
< / div >
< / div >
< div >
< div class = "field-label" > Protocol< / div >
< div class = "field-row" >
< span class = "field-value" id = "connProto" > — < / span >
< / div >
< / div >
< / div >
< button onclick = "copyLndconnectUri()" class = "glass-button w-full mt-2 px-4 py-2-5 rounded-lg text-sm font-medium" id = "copyUriBtn" > Copy lndconnect URI< / button >
< / div >
< / div >
< div class = "help-text" >
Scan the QR code with < strong style = "color:rgba(255,255,255,0.8)" > Zeus< / strong > , < strong style = "color:rgba(255,255,255,0.8)" > Zap< / strong > , or < strong style = "color:rgba(255,255,255,0.8)" > BlueWallet< / strong > to connect. Tor mode recommended for remote access.
< / div >
< / div >
< / div >
<!-- Tabbed Settings Modal -->
< div class = "modal-overlay" id = "settingsModal" >
< div class = "glass-card p-6 modal-content" >
< div class = "flex justify-between items-center mb-4 flex-shrink-0" >
< h2 class = "text-2xl font-bold text-white" > Settings< / h2 >
< button onclick = "closeSettings()" class = "glass-button px-3 py-2 rounded-lg text-xl font-medium" > × < / button >
< / div >
< div class = "flex gap-2 mb-4 flex-shrink-0" style = "background:rgba(255,255,255,0.06);border-radius:0.5rem;padding:0.25rem" >
< button class = "modal-tab active" data-tab = "node" > Node Status< / button >
< button class = "modal-tab" data-tab = "rest" > REST API< / button >
< button class = "modal-tab" data-tab = "grpc" > gRPC< / button >
< button class = "modal-tab" data-tab = "logs" > Logs< / button >
< / div >
< div class = "modal-body" style = "display:flex;flex-direction:column;gap:1rem" >
<!-- Node Status tab -->
< div id = "panel-node" class = "tab-panel active" >
< div class = "flex items-start gap-4 mb-4" >
< div class = "icon-box-sm flex-shrink-0" >
< svg style = "width:1.5rem;height:1.5rem;color:rgba(255,255,255,0.8)" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M13 10V3L4 14h7v7l9-11h-7z" / > < / svg >
< / div >
< div class = "flex-1" >
< h3 class = "text-lg font-semibold text-white mb-1" > Node Status< / h3 >
< p class = "text-white-70 text-sm" > Lightning node information< / p >
< / div >
< / div >
< div style = "display:flex;flex-direction:column;gap:0.75rem" >
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > Node Status< / span >
< span class = "text-green text-sm font-medium" id = "modalNodeStatus" > — < / span >
< / div >
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > Network< / span >
< span class = "text-white-60 text-sm" id = "modalNetwork" > — < / span >
< / div >
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > Version< / span >
< span class = "text-white-60 text-sm" id = "modalVersion" > — < / span >
< / div >
< / div >
< div style = "display:flex;flex-direction:column;gap:0.75rem;margin-top:1rem" >
< div class = "stat-row" style = "flex-direction:column;align-items:flex-start" >
< div class = "font-semibold text-white mb-1" > Network Mode< / div >
< div class = "text-white-70 text-sm" id = "modalNetworkMode" > — < / div >
< / div >
< div class = "stat-row" style = "flex-direction:column;align-items:flex-start" >
< div class = "font-semibold text-white mb-1" > Bitcoin Backend< / div >
< div class = "text-white-70 text-sm" id = "modalBitcoinBackend" > — < / div >
< / div >
< / div >
< / div >
<!-- REST API tab -->
< div id = "panel-rest" class = "tab-panel" >
< div class = "flex items-start gap-4 mb-4" >
< div class = "icon-box-sm flex-shrink-0" >
< svg style = "width:1.5rem;height:1.5rem;color:rgba(255,255,255,0.8)" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01" / > < / svg >
< / div >
< div class = "flex-1" >
< h3 class = "text-lg font-semibold text-white mb-1" > REST API< / h3 >
< p class = "text-white-70 text-sm" > HTTP REST API access< / p >
< / div >
< / div >
< div style = "display:flex;flex-direction:column;gap:0.75rem" >
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > REST Endpoint< / span >
< span class = "text-white-60 text-sm font-mono" id = "modalRestEndpoint" > — < / span >
< / div >
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > API Status< / span >
< span class = "text-green text-sm font-medium" id = "modalRestStatus" > — < / span >
< / div >
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > API Version< / span >
< span class = "text-white-60 text-sm" id = "modalRestVersion" > v1< / span >
< / div >
< / div >
< button class = "info-card-button w-full mt-4 text-sm font-medium py-3 rounded-lg" onclick = "copyRESTInfo()" style = "text-align:center;display:block" > Copy REST Info< / button >
< / div >
<!-- gRPC tab -->
< div id = "panel-grpc" class = "tab-panel" >
< div class = "flex items-start gap-4 mb-4" >
< div class = "icon-box-sm flex-shrink-0" >
< svg style = "width:1.5rem;height:1.5rem;color:rgba(255,255,255,0.8)" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01" / > < / svg >
< / div >
< div class = "flex-1" >
< h3 class = "text-lg font-semibold text-white mb-1" > gRPC Connection< / h3 >
< p class = "text-white-70 text-sm" > High-performance gRPC API< / p >
< / div >
< / div >
< div style = "display:flex;flex-direction:column;gap:0.75rem" >
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > gRPC Host< / span >
< span class = "text-white-60 text-sm font-mono" id = "modalGrpcHost" > — < / span >
< / div >
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > gRPC Status< / span >
< span class = "text-green text-sm font-medium" id = "modalGrpcStatus" > — < / span >
< / div >
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > P2P Port< / span >
< span class = "text-white-60 text-sm font-mono" > 9735< / span >
< / div >
< / div >
< / div >
<!-- Logs tab -->
< div id = "panel-logs" class = "tab-panel" >
< div class = "flex justify-between items-center mb-3" >
< h3 class = "text-lg font-semibold text-white" > Node Logs< / h3 >
< button onclick = "loadLogs()" class = "glass-button px-3 py-1-5 rounded-lg text-xs font-medium" > Refresh< / button >
< / div >
< div class = "logs-box" id = "logsContent" > Loading logs...< / div >
< / div >
< / div >
< / div >
< / div >
< script src = "qrcode.js" > < / script >
< script >
const REST_PORT = 8080;
const GRPC_PORT = 10009;
const P2P_PORT = 9735;
const host = window.location.hostname;
function getBackendUrl() {
2026-06-19 05:03:18 -04:00
// Same-origin by default: the app's own nginx (:18083) proxies these
// paths to the archipelago backend, so a relative base ('') avoids
// any cross-origin/CORS issues (which broke this on http-only nodes).
// ?backend=http://HOST:5678 still overrides for local dev.
2026-03-19 17:02:17 +00:00
const params = new URLSearchParams(window.location.search);
2026-06-19 05:03:18 -04:00
return params.get('backend') || '';
2026-03-19 17:02:17 +00:00
}
function setSettingsTab(tabId) {
document.querySelectorAll('.modal-tab').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-tab') === tabId);
});
document.querySelectorAll('.tab-panel').forEach(panel => {
panel.classList.toggle('active', panel.id === 'panel-' + tabId);
});
if (tabId === 'logs') loadLogs();
}
document.querySelectorAll('.modal-tab').forEach(btn => {
btn.addEventListener('click', () => setSettingsTab(btn.getAttribute('data-tab')));
});
function copyRESTInfo() {
const endpoint = host + ':' + REST_PORT;
const info = 'REST API: http://' + endpoint + '\nAPI Version: v1';
navigator.clipboard.writeText(info).then(() => alert('REST info copied to clipboard!'));
}
function openSettings() {
document.getElementById('settingsModal').classList.add('visible');
}
function closeSettings() {
document.getElementById('settingsModal').classList.remove('visible');
}
function applyLiveData(data) {
if (data.getinfo) {
const g = data.getinfo;
const status = g.synced_to_chain ? 'Running' : 'Waiting for chain…';
const network = (g.chains & & g.chains[0]) ? g.chains[0].network || '—' : '—';
const version = g.version || '—';
setText('headerNetwork', 'Network: ' + network);
setText('summaryNodeStatus', status);
setText('modalNodeStatus', status);
setText('modalNetwork', network);
setText('modalVersion', version);
setText('modalNetworkMode', network);
setText('modalBitcoinBackend', '—');
document.getElementById('statusPing').style.display = g.synced_to_chain ? 'block' : 'none';
}
if (data.channelCount !== undefined) {
setText('channelCount', String(data.channelCount));
}
setText('modalRestEndpoint', host + ':' + REST_PORT);
setText('modalRestStatus', data.restReachable ? 'Active' : '—');
setText('summaryRestStatus', data.restReachable ? 'Active' : '—');
setText('modalGrpcHost', host + ':' + GRPC_PORT);
setText('modalGrpcStatus', data.grpcReachable ? 'Connected' : '—');
setText('summaryGrpcStatus', data.grpcReachable ? 'Connected' : '—');
}
function setText(id, text) {
const el = document.getElementById(id);
if (el) el.textContent = text;
}
async function loadLogs() {
const logsContent = document.getElementById('logsContent');
const backendUrl = getBackendUrl();
2026-06-19 05:03:18 -04:00
logsContent.textContent = 'Loading logs...';
try {
const res = await fetch(backendUrl + '/api/container/logs?app_id=lnd& lines=200', { credentials: 'include' });
if (!res.ok) throw new Error(res.statusText);
const json = await res.json();
const lines = json.result || json.logs || (Array.isArray(json) ? json : []);
logsContent.textContent = Array.isArray(lines) ? lines.join('\n') : String(lines);
} catch (e) {
logsContent.textContent = 'Could not load logs: ' + e.message;
2026-03-19 17:02:17 +00:00
}
}
async function fetchLiveData() {
const backendUrl = getBackendUrl();
const data = { channelCount: 0, restReachable: false, grpcReachable: false };
2026-06-19 05:03:18 -04:00
try {
const getinfoRes = await fetch(backendUrl + '/proxy/lnd/v1/getinfo', { credentials: 'include' });
if (getinfoRes.ok) {
data.getinfo = await getinfoRes.json();
data.restReachable = true;
}
} catch (_) {}
try {
const chRes = await fetch(backendUrl + '/proxy/lnd/v1/channels', { credentials: 'include' });
if (chRes.ok) {
const ch = await chRes.json();
data.channelCount = (ch.channels & & ch.channels.length) || 0;
}
} catch (_) {}
data.grpcReachable = data.restReachable;
2026-03-19 17:02:17 +00:00
applyLiveData(data);
}
document.addEventListener('DOMContentLoaded', () => {
fetchLiveData();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeSettings();
});
document.getElementById('settingsModal').addEventListener('click', (e) => {
if (e.target.id === 'settingsModal') closeSettings();
});
// --- Connect Your Wallet ---
let lndConnInfo = null;
function renderQR(containerId, text) {
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = '';
try {
const qr = qrcode(0, 'L');
qr.addData(text);
qr.make();
container.innerHTML = qr.createImgTag(3, 0);
} catch(e) {
container.innerHTML = '< div style = "color:#999;font-size:11px;text-align:center;padding:2rem" > QR too large for this mode< / div > ';
}
}
function buildLndconnectUri(connHost, connPort, cert, macaroon, isTor) {
let uri = 'lndconnect://' + connHost + ':' + connPort + '?';
if (!isTor & & cert) uri += 'cert=' + cert + '&';
uri += 'macaroon=' + macaroon;
return uri;
}
function updateConnInfo() {
if (!lndConnInfo) return;
const mode = document.getElementById('connMode').value;
const isTor = mode.includes('tor');
const isRest = mode.includes('rest');
const port = isRest ? lndConnInfo.rest_port : lndConnInfo.grpc_port;
const connHost = isTor & & lndConnInfo.tor_onion ? lndConnInfo.tor_onion : host;
const proto = isRest ? 'REST' : 'gRPC';
setText('connHost', connHost);
setText('connPort', String(port));
setText('connProto', proto);
if (isTor & & !lndConnInfo.tor_onion) {
document.getElementById('lndQrBox').innerHTML = '< div style = "color:#999;font-size:12px;text-align:center;padding:2rem" > Tor not configured for LND< / div > ';
document.getElementById('connSubtitle').textContent = 'Tor hidden service not available. Use Local Network mode.';
return;
}
document.getElementById('connSubtitle').textContent = 'Scan QR with Zeus, Zap, or BlueWallet to connect.';
const uri = buildLndconnectUri(connHost, port, lndConnInfo.cert_base64url, lndConnInfo.macaroon_base64url, isTor);
renderQR('lndQrBox', uri);
}
function copyEl(id, btn) {
const text = document.getElementById(id).textContent.trim();
if (!text || text === '—') return;
navigator.clipboard.writeText(text).then(() => {
const orig = btn.innerHTML;
btn.innerHTML = '< svg width = "16" height = "16" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M5 13l4 4L19 7" / > < / svg > ';
btn.style.color = '#4ade80';
setTimeout(() => { btn.innerHTML = orig; btn.style.color = ''; }, 1500);
});
}
function copyLndconnectUri() {
if (!lndConnInfo) return;
const mode = document.getElementById('connMode').value;
const isTor = mode.includes('tor');
const isRest = mode.includes('rest');
const port = isRest ? lndConnInfo.rest_port : lndConnInfo.grpc_port;
const connHost = isTor & & lndConnInfo.tor_onion ? lndConnInfo.tor_onion : host;
const uri = buildLndconnectUri(connHost, port, lndConnInfo.cert_base64url, lndConnInfo.macaroon_base64url, isTor);
const btn = document.getElementById('copyUriBtn');
navigator.clipboard.writeText(uri).then(() => {
const orig = btn.textContent;
btn.textContent = 'Copied!';
btn.style.color = '#4ade80';
setTimeout(() => { btn.textContent = orig; btn.style.color = ''; }, 1500);
});
}
async function fetchConnectInfo() {
try {
2026-06-19 05:03:18 -04:00
const resp = await fetch(getBackendUrl() + '/lnd-connect-info', { credentials: 'include' });
2026-03-19 17:02:17 +00:00
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const data = await resp.json();
if (data.cert_base64url) {
lndConnInfo = data;
// Auto-select local mode when Tor is not available
if (!data.tor_onion) {
document.getElementById('connMode').value = 'rest-local';
document.getElementById('connSubtitle').textContent = 'Tor hidden service not available. Use Local Network mode.';
}
try { updateConnInfo(); } catch(ue) {
document.getElementById('lndQrBox').innerHTML = '< div style = "color:#f87171;font-size:11px;text-align:center;padding:1rem" > Update error: ' + ue.message + '< / div > ';
return;
}
} else if (data.error) {
throw new Error(data.error);
}
} catch(e) {
document.getElementById('lndQrBox').innerHTML = '< div style = "color:#f87171;font-size:12px;text-align:center;padding:2rem" > ' + e.message + '< / div > ';
}
}
fetchConnectInfo();
< / script >
< / body >
< / html >