archy/docker/lnd-ui/index.html
Dorian 1f0d51865d fix: LND UI CSS, QR codes, services tab, wallet creation, tx filtering
- LND UI: replace cdn.tailwindcss.com with local tailwind.css (CSP fix)
- LND UI: make asset paths relative for nginx proxy compatibility
- Web5 wallet: add QR code for on-chain receive addresses (qrcode npm)
- Web5 wallet: hide incoming transactions after 3 confirmations
- Apps: add "Services" tab to separate backend containers from user apps
- Home: null guard on packages.value to prevent TypeError on load
- First-boot: auto-create Bitcoin Knots wallet (no longer auto-created)
- AppSession: add mempool-electrs to port mapping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 15:34:04 +00:00

752 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LND - Archipelago</title>
<link rel="stylesheet" href="tailwind.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', sans-serif;
min-height: 100vh;
color: white;
overflow-x: hidden;
}
.bg-perspective-container {
position: fixed;
inset: 0;
z-index: -10;
perspective: 1000px;
perspective-origin: 50% 50%;
overflow: hidden;
}
.bg-layer {
position: absolute;
inset: 0;
background-image: url('assets/img/bg-intro.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transition: all 0.45s cubic-bezier(0.68, -0.55, 0.265, 1.55);
transform-style: preserve-3d;
opacity: 1;
transform: translateZ(0) scale(1);
}
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
z-index: -5;
pointer-events: none;
}
/* Glass card - Archipelago standard with gradient border */
.glass-card {
position: relative;
background: rgba(0, 0, 0, 0.60);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.22);
border-radius: 1rem;
overflow-x: hidden;
overflow-y: visible;
border: none;
}
.glass-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 2px;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), transparent);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
z-index: 1;
}
.glass-card > * {
position: relative;
z-index: 2;
}
/* Glass button - Archipelago standard (secondary actions) */
.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;
}
.glass-button:hover {
color: white;
background-color: rgba(0, 0, 0, 0.7);
}
/* Info card - Archipelago standard (display only, no hover) */
.info-card {
position: relative;
background: rgba(0, 0, 0, 0.60);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.22);
border-radius: 16px;
padding: 12px;
border: none;
}
.info-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 2px;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), transparent);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
/* Interactive button - Same as info-card but with hover effects */
.info-card-button {
position: relative;
background: rgba(0, 0, 0, 0.60);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.22);
border-radius: 16px;
padding: 12px;
transition: all 0.3s ease;
border: none;
cursor: pointer;
color: rgba(255, 255, 255, 0.9);
}
.info-card-button::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 2px;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), transparent);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
transition: all 0.3s ease;
}
.info-card-button:hover {
transform: translateY(-2px);
background: rgba(0, 0, 0, 0.35);
box-shadow:
0 12px 32px rgba(0, 0, 0, 0.6),
inset 0 1px 0 rgba(255, 255, 255, 0.25);
color: rgba(255, 255, 255, 1);
}
.info-card-button:hover::before {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), transparent);
}
.info-card-button:active {
transform: translateY(1px);
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
padding-bottom: 4rem;
}
/* Logo gradient border */
.logo-gradient-border {
position: relative;
border-radius: 16px;
padding: 3px;
background: linear-gradient(135deg, rgba(139, 92, 246, 0.8) 0%, rgba(0, 0, 0, 0.8) 100%);
box-shadow: 0 8px 24px rgba(139, 92, 246, 0.5);
display: inline-block;
}
.logo-gradient-border::after {
content: '';
position: absolute;
inset: 3px;
border-radius: 13px;
background: rgba(0, 0, 0, 0.4);
z-index: 0;
}
.logo-gradient-border img,
.logo-gradient-border svg {
border-radius: 13px;
display: block;
position: relative;
z-index: 1;
width: 64px;
height: 64px;
}
@keyframes ping {
75%, 100% {
transform: scale(2);
opacity: 0;
}
}
.animate-ping {
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
}
.modal-tab { transition: all 0.2s ease; }
.modal-tab.active { background: rgba(255,255,255,0.2); color: white; }
.modal-tab:not(.active) { color: rgba(255,255,255,0.6); }
.modal-tab:not(.active):hover { color: rgba(255,255,255,0.9); }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
</style>
</head>
<body>
<div class="bg-perspective-container">
<div class="bg-layer"></div>
</div>
<div class="overlay"></div>
<div class="container">
<!-- Header - Glass card with logo and Settings button top-right -->
<div class="glass-card p-6 mb-6">
<div class="flex items-start justify-between gap-4">
<div class="flex items-start gap-4 flex-1 min-w-0">
<div class="flex-shrink-0">
<div class="logo-gradient-border">
<img
src="assets/img/app-icons/lnd.svg"
alt="LND"
class="w-16 h-16"
style="object-fit: contain; padding: 8px;"
/>
</div>
</div>
<div class="flex-1 min-w-0">
<h1 class="text-3xl font-bold text-white mb-2">LND</h1>
<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>
</div>
<button
onclick="openSettings()"
class="flex-shrink-0 flex items-center gap-2 px-4 py-2.5 glass-button rounded-lg text-sm font-medium"
>
<svg class="w-5 h-5" 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-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="info-card flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="relative">
<div class="w-3 h-3 rounded-full bg-green-400" id="statusDot"></div>
<div class="absolute inset-0 w-3 h-3 rounded-full bg-green-400 animate-ping opacity-75" id="statusPing" style="display:none"></div>
</div>
<div>
<p class="text-sm font-medium text-white">Node Status</p>
<p class="text-xs text-white/60" id="summaryNodeStatus"></p>
</div>
</div>
</div>
<div class="info-card flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-2xl text-orange-500 font-bold">&#9889;</span>
<div>
<p class="text-sm font-medium text-white">Channels</p>
<p class="text-xs text-orange-500 font-medium" id="channelCount">0</p>
</div>
</div>
</div>
<div class="info-card flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="relative">
<div class="w-3 h-3 rounded-full bg-green-400" id="restDot"></div>
</div>
<div>
<p class="text-sm font-medium text-white">REST API</p>
<p class="text-xs text-white/60" id="summaryRestStatus"></p>
</div>
</div>
<button onclick="openSettings(); setSettingsTab('rest');" class="px-3 py-1.5 glass-button rounded 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="relative">
<div class="w-3 h-3 rounded-full bg-green-400" id="grpcDot"></div>
</div>
<div>
<p class="text-sm font-medium text-white">gRPC</p>
<p class="text-xs text-white/60" id="summaryGrpcStatus"></p>
</div>
</div>
<button onclick="openSettings(); setSettingsTab('logs');" class="px-3 py-1.5 glass-button rounded text-xs font-medium">Logs</button>
</div>
</div>
</div>
<!-- Main content: placeholder for wallet (balance, Receive, Send, recent activity) -->
<div class="glass-card p-6 mb-8">
<h2 class="text-xl font-semibold text-white mb-4">Wallet</h2>
<p class="text-white/60 text-sm mb-4">Balance, Receive, and Send will appear here when connected to your node.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="p-4 bg-white/5 rounded-lg">
<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="p-4 bg-white/5 rounded-lg">
<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="p-4 bg-white/5 rounded-lg">
<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="px-6 py-3 glass-button rounded-lg font-medium text-white/90 hover:text-white transition-colors" disabled>Receive</button>
<button class="px-6 py-3 glass-button rounded-lg font-medium text-white/90 hover:text-white transition-colors" 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="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg text-white text-sm font-medium appearance-none cursor-pointer focus:outline-none focus:border-white/25" style="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;">
<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="flex flex-col sm:flex-row gap-6" id="connDisplay">
<div class="flex-shrink-0 w-48 h-48 bg-white rounded-xl flex items-center justify-center p-2" id="lndQrBox">
<div class="text-gray-400 text-xs text-center">Loading...</div>
</div>
<div class="flex-1 space-y-3">
<div>
<div class="text-[11px] font-semibold uppercase tracking-wide text-white/45 mb-1">Host</div>
<div class="flex items-center bg-white/5 border border-white/10 rounded-lg overflow-hidden">
<span class="flex-1 px-3 py-2.5 font-mono text-sm text-white/90 truncate" id="connHost"></span>
<button onclick="copyEl('connHost', this)" class="px-3 py-2.5 border-l border-white/10 text-white/40 hover:text-white/80 hover:bg-white/5 transition-colors">
<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" 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="flex gap-3">
<div class="flex-1">
<div class="text-[11px] font-semibold uppercase tracking-wide text-white/45 mb-1">Port</div>
<div class="flex items-center bg-white/5 border border-white/10 rounded-lg overflow-hidden">
<span class="flex-1 px-3 py-2.5 font-mono text-sm text-white/90" id="connPort"></span>
<button onclick="copyEl('connPort', this)" class="px-3 py-2.5 border-l border-white/10 text-white/40 hover:text-white/80 hover:bg-white/5 transition-colors">
<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" 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="flex-1">
<div class="text-[11px] font-semibold uppercase tracking-wide text-white/45 mb-1">Protocol</div>
<div class="flex items-center bg-white/5 border border-white/10 rounded-lg overflow-hidden">
<span class="flex-1 px-3 py-2.5 font-mono text-sm text-white/90" id="connProto"></span>
</div>
</div>
</div>
<button onclick="copyLndconnectUri()" class="w-full mt-2 px-4 py-2.5 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors" id="copyUriBtn">Copy lndconnect URI</button>
</div>
</div>
<div class="mt-4 pt-4 border-t border-white/8 text-sm text-white/50 leading-relaxed">
Scan the QR code with <strong class="text-white/80">Zeus</strong>, <strong class="text-white/80">Zap</strong>, or <strong class="text-white/80">BlueWallet</strong> to connect. Tor mode recommended for remote access.
</div>
</div>
</div>
<!-- Tabbed Settings Modal -->
<div class="modal hidden fixed inset-0 bg-black/80 backdrop-blur-sm z-50 items-center justify-center p-4" id="settingsModal">
<div class="glass-card p-6 max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col">
<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 glass-card p-2 rounded-lg">
<button class="modal-tab flex-1 px-4 py-2 rounded-lg text-sm font-medium active" data-tab="node">Node Status</button>
<button class="modal-tab flex-1 px-4 py-2 rounded-lg text-sm font-medium" data-tab="rest">REST API</button>
<button class="modal-tab flex-1 px-4 py-2 rounded-lg text-sm font-medium" data-tab="grpc">gRPC</button>
<button class="modal-tab flex-1 px-4 py-2 rounded-lg text-sm font-medium" data-tab="logs">Logs</button>
</div>
<div class="overflow-y-auto flex-1 min-h-0 space-y-4">
<!-- Node Status tab -->
<div id="panel-node" class="tab-panel active">
<div class="flex items-start gap-4 mb-4">
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
<svg class="w-6 h-6 text-white/80" 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 class="space-y-3">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">Node Status</span>
<span class="text-green-400 text-sm font-medium" id="modalNodeStatus"></span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">Network</span>
<span class="text-white/60 text-sm" id="modalNetwork"></span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">Version</span>
<span class="text-white/60 text-sm" id="modalVersion"></span>
</div>
</div>
<div class="mt-4 space-y-3">
<div class="p-3 bg-white/5 rounded-lg">
<div class="font-semibold text-white mb-1">Network Mode</div>
<div class="text-white/70 text-sm" id="modalNetworkMode"></div>
</div>
<div class="p-3 bg-white/5 rounded-lg">
<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="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
<svg class="w-6 h-6 text-white/80" 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 class="space-y-3">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<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="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">API Status</span>
<span class="text-green-400 text-sm font-medium" id="modalRestStatus"></span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<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="mt-4 w-full info-card-button text-sm font-medium py-3 rounded-lg" onclick="copyRESTInfo()">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="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
<svg class="w-6 h-6 text-white/80" 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 class="space-y-3">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<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="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">gRPC Status</span>
<span class="text-green-400 text-sm font-medium" id="modalGrpcStatus"></span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<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="px-3 py-1.5 glass-button rounded text-xs font-medium">Refresh</button>
</div>
<div class="bg-black/40 rounded-lg p-4 font-mono text-xs text-white/80 whitespace-pre-wrap break-all min-h-[200px]" 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() {
const params = new URLSearchParams(window.location.search);
return params.get('backend') || '';
}
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.remove('hidden');
document.getElementById('settingsModal').classList.add('flex');
}
function closeSettings() {
document.getElementById('settingsModal').classList.add('hidden');
document.getElementById('settingsModal').classList.remove('flex');
}
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();
if (backendUrl) {
logsContent.textContent = 'Loading logs...';
try {
const res = await fetch(backendUrl + '/api/container/logs?app_id=lnd&lines=200');
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;
}
} else {
logsContent.textContent = 'Open this app with ?backend=http://HOST:5678 to load logs from the server.';
}
}
async function fetchLiveData() {
const backendUrl = getBackendUrl();
const data = { channelCount: 0, restReachable: false, grpcReachable: false };
if (backendUrl) {
try {
const getinfoRes = await fetch(backendUrl + '/proxy/lnd/v1/getinfo');
if (getinfoRes.ok) {
data.getinfo = await getinfoRes.json();
data.restReachable = true;
}
} catch (_) {}
try {
const chRes = await fetch(backendUrl + '/proxy/lnd/v1/channels');
if (chRes.ok) {
const ch = await chRes.json();
data.channelCount = (ch.channels && ch.channels.length) || 0;
}
} catch (_) {}
data.grpcReachable = data.restReachable;
}
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 {
const resp = await fetch('http://' + window.location.hostname + '/lnd-connect-info');
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const data = await resp.json();
if (data.cert_base64url) {
lndConnInfo = data;
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>