- 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>
752 lines
37 KiB
HTML
752 lines
37 KiB
HTML
<!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">⚡</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>
|