archy/docker/lnd-ui/index.html

597 lines
28 KiB
HTML
Raw Normal View History

<!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>
<script src="https://cdn.tailwindcss.com"></script>
<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;
2026-01-28 00:47:00 +00:00
background-image: url('/assets/img/bg-web5.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;
2026-01-28 00:47:00 +00:00
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" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 2.31.903 2.31.903a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.903 2.31-.903 2.31a1.724 1.724 0 002.572 1.065c.426 1.756 2.924 1.756 3.35 0a1.724 1.724 0 002.573-1.066c1.543.94 2.31-.903 2.31-.903a1.724 1.724 0 001.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.903-2.31.903-2.31a1.724 1.724 0 00-2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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>
</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>
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();
});
</script>
</body>
</html>