archy/docker/bitcoin-ui/index.html

1304 lines
63 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">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title id="pageTitle">Bitcoin Node - 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;
background: #000;
color: white;
overflow-x: hidden;
}
/* Background - Web5 style */
.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-network.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);
}
/* Dark overlay - Web5 style (0.8 opacity) */
.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);
}
/* Gradient button - Archipelago standard (primary actions) */
.gradient-button {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(0, 0, 0, 0.8) 100%);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.95);
transition: all 0.3s ease;
}
.gradient-button:hover {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(0, 0, 0, 0.9) 100%);
border-color: rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
transform: translateY(-1px);
}
.gradient-button:active {
transform: translateY(1px);
}
/* Interactive 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 */
.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(255, 255, 255, 0.6) 0%, rgba(0, 0, 0, 0.8) 100%);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
display: inline-block;
}
.logo-gradient-border::after {
content: '';
position: absolute;
inset: 3px;
border-radius: 13px;
background: #fff;
z-index: 0;
}
.logo-gradient-border img {
border-radius: 13px;
display: block;
position: relative;
z-index: 1;
width: 64px;
height: 64px;
}
/* Ping animation for status dots */
@keyframes ping {
75%, 100% {
transform: scale(2);
opacity: 0;
}
}
.animate-ping {
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
}
/* Pulsing glow for progress bar */
@keyframes progressGlow {
0%, 100% {
box-shadow: 0 0 10px rgba(251, 146, 60, 0.5),
0 0 20px rgba(251, 146, 60, 0.3),
0 0 30px rgba(251, 146, 60, 0.1);
}
50% {
box-shadow: 0 0 20px rgba(251, 146, 60, 0.8),
0 0 30px rgba(251, 146, 60, 0.5),
0 0 40px rgba(251, 146, 60, 0.3);
}
}
.progress-glow {
animation: progressGlow 2s ease-in-out infinite;
}
/* Spinning animation */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin-slow {
animation: spin 3s linear infinite;
}
/* Shimmer effect */
@keyframes shimmer {
0% {
background-position: -1000px 0;
}
100% {
background-position: 1000px 0;
}
}
.shimmer {
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.1) 50%,
rgba(255, 255, 255, 0) 100%
);
background-size: 1000px 100%;
animation: shimmer 3s infinite;
}
/* Number increment animation */
@keyframes numberPulse {
0%, 100% {
transform: scale(1);
color: rgba(255, 255, 255, 0.9);
}
50% {
transform: scale(1.05);
color: rgba(251, 146, 60, 1);
}
}
.number-update {
animation: numberPulse 0.5s ease-in-out;
}
</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 node info -->
<div class="glass-card p-6 mb-6">
<div class="flex flex-col md:flex-row items-center md:items-center gap-4 md:gap-6">
<!-- Logo - Top Left -->
<div class="flex-shrink-0">
<div class="logo-gradient-border">
<img
id="implLogo"
src="/assets/img/app-icons/bitcoin-knots.webp"
alt="Bitcoin Node"
class="w-16 h-16"
style="object-fit: contain;"
onerror="this.style.display='none'"
/>
</div>
</div>
<!-- Title and Description -->
<div class="flex-1 min-w-0">
<h1 id="implName" class="text-3xl font-bold text-white mb-2">Bitcoin Node</h1>
<p id="implTagline" class="text-white/70">Detecting implementation…</p>
</div>
<!-- Node Status Info - Compact on Desktop -->
<div class="w-full md:w-auto flex flex-col md:flex-row gap-3 md:gap-4 mt-4 md:mt-0">
<div class="info-card flex items-center gap-3">
<div class="relative">
<div class="w-3 h-3 rounded-full bg-green-400"></div>
<div class="absolute inset-0 w-3 h-3 rounded-full bg-green-400 animate-ping opacity-75"></div>
</div>
<div>
<p class="text-xs text-white/60">Status</p>
<p class="text-sm font-medium text-white">Running</p>
</div>
</div>
<div class="info-card flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" 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>
<p class="text-xs text-white/60">Version</p>
<p class="text-sm font-medium text-white" id="nodeVersion">Loading...</p>
</div>
</div>
<div class="info-card flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
<div>
<p class="text-xs text-white/60">Network</p>
<p class="text-sm font-medium text-white" id="networkType">Loading...</p>
</div>
</div>
<div class="info-card flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2 1.6 3 4 3h8c2.4 0 4-1 4-3V7M4 7c0-2 1.6-3 4-3h8c2.4 0 4 1 4 3M4 7h16M9 11h6M9 15h6" />
</svg>
<div>
<p class="text-xs text-white/60">Storage</p>
<p class="text-sm font-medium text-white" id="storageMode">Loading...</p>
</div>
</div>
<button
onclick="openSettings()"
class="px-4 py-3 glass-button rounded-lg text-sm font-medium"
>
Settings
</button>
</div>
</div>
</div>
<!-- Blockchain Sync Status Card - NEW -->
<div class="glass-card p-6 mb-6" id="syncStatusCard">
<div class="flex items-start gap-4 mb-4">
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-orange-500/20 flex items-center justify-center">
<svg class="w-6 h-6 text-orange-500 animate-spin-slow" fill="none" stroke="currentColor" viewBox="0 0 24 24" id="syncIcon">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<div class="flex-1">
<h2 class="text-xl font-semibold text-white mb-1">Blockchain Sync</h2>
<p class="text-white/70 text-sm" id="syncStatusText">Checking sync status...</p>
</div>
</div>
<!-- Progress Bar -->
<div class="mb-4">
<div class="flex justify-between text-sm text-white/60 mb-2">
<span id="currentBlock">Block 0</span>
<span id="syncPercentage">0%</span>
</div>
<div class="w-full bg-white/10 rounded-full h-3 overflow-hidden relative shimmer">
<div class="h-full bg-gradient-to-r from-orange-500 to-yellow-400 rounded-full transition-all duration-500 progress-glow" id="syncProgressBar" style="width: 0%"></div>
</div>
</div>
<!-- Sync Stats Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="info-card">
<p class="text-xs text-white/60 mb-1">Current Height</p>
<p class="text-lg font-semibold text-white transition-all" id="currentHeight">-</p>
</div>
<div class="info-card">
<p class="text-xs text-white/60 mb-1">Network Height</p>
<p class="text-lg font-semibold text-white" id="networkHeight">-</p>
</div>
<div class="info-card">
<p class="text-xs text-white/60 mb-1">Headers</p>
<p class="text-lg font-semibold text-white" id="headers">-</p>
</div>
<div class="info-card">
<p class="text-xs text-white/60 mb-1">Verification</p>
<p class="text-lg font-semibold text-white" id="verificationProgress">-</p>
</div>
</div>
</div>
<!-- Core Services Overview Cards - Web5 style -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<div class="glass-card p-6">
<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.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
</svg>
</div>
<div class="flex-1">
<h2 class="text-xl font-semibold text-white mb-2">RPC Connection</h2>
<p class="text-white/70 text-sm mb-4">JSON-RPC API access</p>
</div>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
<span class="text-white/80 text-sm">RPC Host</span>
</div>
<span class="text-white/60 text-sm font-mono" id="rpcHost">localhost:8332</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-white/80 text-sm">RPC User</span>
</div>
<span class="text-white/60 text-sm font-mono">archipelago</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<span class="text-white/80 text-sm">RPC Status</span>
</div>
<span class="text-green-400 text-sm font-medium">Connected</span>
</div>
</div>
<button class="mt-4 w-full info-card-button text-sm font-medium" onclick="copyRPCInfo()">
Copy RPC Info
</button>
</div>
<!-- ZMQ Notifications -->
<div class="glass-card p-6">
<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="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
</div>
<div class="flex-1">
<h2 class="text-xl font-semibold text-white mb-2">ZMQ Notifications</h2>
<p class="text-white/70 text-sm mb-4">Real-time block and transaction updates</p>
</div>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
</svg>
<span class="text-white/80 text-sm">Block Notifications</span>
</div>
<span class="text-white/60 text-sm font-mono">tcp://localhost:28332</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<span class="text-white/80 text-sm">TX Notifications</span>
</div>
<span class="text-white/60 text-sm font-mono">tcp://localhost:28333</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<span class="text-white/80 text-sm">ZMQ Status</span>
</div>
<span class="text-green-400 text-sm font-medium">Active</span>
</div>
</div>
<button class="mt-4 w-full info-card-button text-sm font-medium" onclick="openLogs()">
View Logs
</button>
</div>
</div>
<div class="glass-card p-6 mb-8">
<div class="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4 mb-6">
<div>
<h2 class="text-xl font-semibold text-white mb-2">Transaction Relay Sharing</h2>
<p class="text-white/70 text-sm">Trusted peer access for broadcasting transactions through this node</p>
</div>
<div class="px-3 py-2 bg-white/5 rounded-lg text-sm">
<span class="text-white/60">Local node</span>
<span class="ml-2 font-medium text-yellow-300" id="relaySyncStatus">Checking...</span>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-5">
<div class="p-4 bg-white/5 rounded-lg">
<div class="text-xs uppercase tracking-wide text-white/50 mb-2">HTTPS Endpoint</div>
<div class="text-sm text-white/80 font-mono break-all min-h-[1.5rem]" id="relayHttpsEndpoint">Not configured</div>
</div>
<div class="p-4 bg-white/5 rounded-lg">
<div class="text-xs uppercase tracking-wide text-white/50 mb-2">HTTP Endpoint</div>
<div class="text-sm text-white/80 font-mono break-all min-h-[1.5rem]" id="relayHttpEndpoint">Not configured</div>
</div>
<div class="p-4 bg-white/5 rounded-lg">
<div class="text-xs uppercase tracking-wide text-white/50 mb-2">Tor Endpoint</div>
<div class="text-sm text-white/80 font-mono break-all min-h-[1.5rem]" id="relayTorEndpoint">Not configured</div>
</div>
</div>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<label class="flex items-center justify-between gap-3 p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">Allow peer use</span>
<input id="relayEnabledToggle" type="checkbox" class="h-5 w-5 accent-orange-500" onchange="saveRelaySettings()">
</label>
<label class="flex items-center justify-between gap-3 p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">Allow requests</span>
<input id="relayRequestsToggle" type="checkbox" class="h-5 w-5 accent-orange-500" onchange="saveRelaySettings()">
</label>
<label class="flex items-center justify-between gap-3 p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">Serve over Tor</span>
<input id="relayTorToggle" type="checkbox" class="h-5 w-5 accent-orange-500" onchange="saveRelaySettings()">
</label>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<input id="relayHttpsInput" class="w-full px-3 py-2 rounded-lg bg-black/40 border border-white/10 text-sm text-white placeholder-white/35" placeholder="https://rpc.example.com/">
<input id="relayHttpInput" class="w-full px-3 py-2 rounded-lg bg-black/40 border border-white/10 text-sm text-white placeholder-white/35" placeholder="http://192.168.1.2/">
</div>
<div class="grid grid-cols-1 md:grid-cols-[1fr_auto] gap-3">
<input id="relayTorInput" class="w-full px-3 py-2 rounded-lg bg-black/40 border border-white/10 text-sm text-white placeholder-white/35" placeholder="http://exampleonion.onion/">
<button class="glass-button px-4 py-2 rounded-lg text-sm font-medium" onclick="createRelayTorService()">Create Tor</button>
</div>
<button class="gradient-button px-4 py-2 rounded-lg text-sm font-medium" onclick="saveRelaySettings()">Save Sharing Settings</button>
</div>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-[1fr_auto] gap-3">
<select id="relayPeerSelect" class="w-full px-3 py-2 rounded-lg bg-black/40 border border-white/10 text-sm text-white" onchange="saveRelaySettings()">
<option value="">No trusted nodes available</option>
</select>
<button id="relayRequestButton" class="glass-button px-4 py-2 rounded-lg text-sm font-medium" onclick="requestPeerRelay()">Request Access</button>
</div>
<textarea id="relayRequestMessage" class="w-full px-3 py-2 rounded-lg bg-black/40 border border-white/10 text-sm text-white placeholder-white/35 min-h-[5rem]" placeholder="Optional note for the peer"></textarea>
<div class="p-3 bg-white/5 rounded-lg">
<div class="flex items-center justify-between gap-3">
<span class="text-white/70 text-sm">Restricted RPC user</span>
<span class="text-white/90 text-sm font-mono" id="relayCredentialUser">txrelay</span>
</div>
<div class="text-xs mt-2 text-white/50" id="relayCredentialStatus">Credential status unavailable</div>
</div>
<div>
<div class="text-sm font-semibold text-white mb-2">Relay Requests</div>
<div class="space-y-2" id="relayRequestsList">
<div class="text-sm text-white/50 p-3 bg-white/5 rounded-lg">No relay requests</div>
</div>
</div>
<div class="text-sm text-white/60" id="relayStatusMessage"></div>
</div>
</div>
</div>
</div>
<!-- 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-[80vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-white">Node Settings</h2>
<button onclick="closeSettings()" class="glass-button px-3 py-2 rounded-lg text-xl font-medium">×</button>
</div>
<div class="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="settingsNetworkMode">Loading…</div>
</div>
<div class="p-3 bg-white/5 rounded-lg">
<div class="font-semibold text-white mb-1">Storage Mode</div>
<div class="text-white/70 text-sm" id="settingsStorageMode">Loading…</div>
</div>
<div class="p-3 bg-white/5 rounded-lg">
<div class="font-semibold text-white mb-1">Transaction Index</div>
<div class="text-white/70 text-sm" id="settingsTxIndex">Loading…</div>
</div>
<div class="p-3 bg-white/5 rounded-lg">
<div class="font-semibold text-white mb-1">ZMQ Publishing</div>
<div class="text-white/70 text-sm" id="settingsZmq">Loading…</div>
</div>
<div class="p-3 bg-white/5 rounded-lg">
<div class="font-semibold text-white mb-1">RPC Access</div>
<div class="text-white/70 text-sm" id="settingsRpc">Loading…</div>
</div>
</div>
</div>
</div>
<!-- Logs Modal -->
<div class="modal hidden fixed inset-0 bg-black/80 backdrop-blur-sm z-50 items-center justify-center p-4" id="logsModal">
<div class="glass-card p-6 max-w-4xl w-full max-h-[80vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-white">Node Logs</h2>
<button onclick="closeLogs()" class="glass-button px-3 py-2 rounded-lg text-xl font-medium">×</button>
</div>
<div class="bg-black/40 rounded-lg p-4 font-mono text-xs text-white/80 whitespace-pre-wrap break-all" id="logsContent">
Loading logs...
</div>
</div>
</div>
<script>
console.log('[Bitcoin UI] Script loaded, initializing...');
// RPC Configuration - Use local Nginx proxy within container
const RPC_ENDPOINT = 'bitcoin-rpc/';
const STATUS_ENDPOINT = 'bitcoin-status';
const ARCHY_RPC_ENDPOINT = 'rpc/v1';
console.log('[Bitcoin UI] RPC Endpoint:', RPC_ENDPOINT);
// Make RPC call to Bitcoin node via local proxy
async function callRPC(method, params = []) {
try {
console.log(`[Bitcoin UI] Calling RPC method: ${method}`);
const response = await fetch(RPC_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '1.0',
id: 'bitcoin-ui',
method: method,
params: params
})
});
console.log(`[Bitcoin UI] RPC response status: ${response.status}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(`[Bitcoin UI] RPC ${method} success:`, data.result ? 'OK' : 'Error');
if (data.error) {
throw new Error(data.error.message);
}
return data.result;
} catch (error) {
console.error(`[Bitcoin UI] RPC call failed: ${method}`, error);
return null;
}
}
async function fetchBitcoinStatus() {
const response = await fetch(STATUS_ENDPOINT, { cache: 'no-store' });
if (!response.ok) {
throw new Error(`status HTTP ${response.status}`);
}
return response.json();
}
function cookieValue(name) {
return document.cookie
.split('; ')
.find(row => row.startsWith(`${name}=`))
?.split('=')
.slice(1)
.join('=') || '';
}
async function callArchyRPC(method, params = {}) {
const headers = { 'Content-Type': 'application/json' };
const csrf = cookieValue('csrf');
if (csrf) headers['X-CSRF-Token'] = decodeURIComponent(csrf);
const response = await fetch(ARCHY_RPC_ENDPOINT, {
method: 'POST',
headers,
credentials: 'include',
cache: 'no-store',
body: JSON.stringify({ method, params })
});
const body = await response.json().catch(() => ({}));
if (!response.ok || body.error) {
throw new Error(body.error?.message || `Archipelago RPC ${response.status}`);
}
return body.result;
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[char]));
}
function setText(id, value, fallback = 'Not configured') {
const el = document.getElementById(id);
if (el) el.textContent = value || fallback;
}
function renderRelayRequests(requests = []) {
const list = document.getElementById('relayRequestsList');
if (!list) return;
if (!requests.length) {
list.innerHTML = '<div class="text-sm text-white/50 p-3 bg-white/5 rounded-lg">No relay requests</div>';
return;
}
list.innerHTML = requests.map(req => {
const name = escapeHtml(req.peer_name || req.peer_onion || req.peer_pubkey);
const message = req.message ? `<div class="text-xs text-white/50 mt-1">${escapeHtml(req.message)}</div>` : '';
const endpoint = req.approved_endpoint ? `<div class="text-xs text-white/50 mt-1 font-mono break-all">${escapeHtml(req.approved_endpoint)}</div>` : '';
const statusClass = req.status === 'approved'
? 'text-green-300'
: req.status === 'rejected'
? 'text-red-300'
: 'text-yellow-300';
const actions = req.direction === 'incoming' && req.status === 'pending'
? `<div class="flex gap-2 mt-3">
<button class="glass-button px-3 py-2 rounded-lg text-xs font-medium" onclick="approveRelayRequest('${escapeHtml(req.id)}')">Approve</button>
<button class="glass-button px-3 py-2 rounded-lg text-xs font-medium" onclick="rejectRelayRequest('${escapeHtml(req.id)}')">Reject</button>
</div>`
: '';
return `<div class="p-3 bg-white/5 rounded-lg">
<div class="flex items-center justify-between gap-3">
<div class="text-sm text-white/80">${name}</div>
<div class="text-xs uppercase ${statusClass}">${escapeHtml(req.direction)} · ${escapeHtml(req.status)}</div>
</div>
${message}
${endpoint}
${actions}
</div>`;
}).join('');
}
function renderRelayPeers(peers = [], selectedPeer = '', localSynced = true) {
const select = document.getElementById('relayPeerSelect');
const button = document.getElementById('relayRequestButton');
if (!select) return;
if (!localSynced) {
select.innerHTML = '<option value="">Local Bitcoin node must finish syncing first</option>';
select.disabled = true;
if (button) button.disabled = true;
return;
}
if (!peers.length) {
select.innerHTML = '<option value="">No trusted nodes available</option>';
select.disabled = true;
if (button) button.disabled = true;
return;
}
select.disabled = false;
if (button) button.disabled = false;
select.innerHTML = '<option value="">Choose a trusted node</option>' + peers.map(peer => {
const label = escapeHtml(peer.name || peer.onion || peer.pubkey.slice(0, 16));
const approved = peer.relay_approved ? ' · approved' : '';
const selected = peer.pubkey === selectedPeer ? ' selected' : '';
return `<option value="${escapeHtml(peer.pubkey)}"${selected}>${label}${approved}</option>`;
}).join('');
}
async function loadRelayAccess() {
const statusEl = document.getElementById('relayStatusMessage');
try {
const relay = await callArchyRPC('bitcoin.relay-status');
const settings = relay.settings || {};
const local = relay.local_node || {};
setText('relayHttpsEndpoint', settings.https_endpoint);
setText('relayHttpEndpoint', settings.http_endpoint);
setText('relayTorEndpoint', settings.tor_endpoint);
const syncEl = document.getElementById('relaySyncStatus');
if (syncEl) {
syncEl.textContent = local.synced ? 'Synchronized' : 'Not synchronized';
syncEl.className = local.synced ? 'ml-2 font-medium text-green-300' : 'ml-2 font-medium text-yellow-300';
}
const enabled = document.getElementById('relayEnabledToggle');
const requests = document.getElementById('relayRequestsToggle');
const tor = document.getElementById('relayTorToggle');
if (enabled) enabled.checked = !!settings.enabled_for_peers;
if (requests) requests.checked = !!settings.allow_peer_requests;
if (tor) tor.checked = !!settings.allow_tor;
const httpsInput = document.getElementById('relayHttpsInput');
const httpInput = document.getElementById('relayHttpInput');
const torInput = document.getElementById('relayTorInput');
if (httpsInput && document.activeElement !== httpsInput) httpsInput.value = settings.https_endpoint || '';
if (httpInput && document.activeElement !== httpInput) httpInput.value = settings.http_endpoint || '';
if (torInput && document.activeElement !== torInput) torInput.value = settings.tor_endpoint || '';
renderRelayPeers(relay.trusted_nodes || [], settings.selected_peer_pubkey || '', !!local.synced);
renderRelayRequests(relay.requests || []);
setText('relayCredentialUser', relay.credentials?.username || 'txrelay', 'txrelay');
setText(
'relayCredentialStatus',
relay.credentials?.available ? `Credential file ready: ${relay.credentials.client_env_path}. ${relay.credentials.restart_hint || ''}` : 'Restricted relay credential will be generated when peer sharing is enabled',
'Credential status unavailable'
);
if (statusEl) statusEl.textContent = '';
} catch (error) {
console.warn('[Bitcoin UI] relay status failed', error);
if (statusEl) statusEl.textContent = `Relay controls unavailable: ${error.message}`;
}
}
async function saveRelaySettings() {
const statusEl = document.getElementById('relayStatusMessage');
const payload = {
enabled_for_peers: !!document.getElementById('relayEnabledToggle')?.checked,
allow_peer_requests: !!document.getElementById('relayRequestsToggle')?.checked,
allow_tor: !!document.getElementById('relayTorToggle')?.checked,
allow_https: !!document.getElementById('relayHttpsInput')?.value.trim(),
allow_http: !!document.getElementById('relayHttpInput')?.value.trim(),
selected_peer_pubkey: document.getElementById('relayPeerSelect')?.value || '',
https_endpoint: document.getElementById('relayHttpsInput')?.value.trim() || '',
http_endpoint: document.getElementById('relayHttpInput')?.value.trim() || '',
tor_endpoint: document.getElementById('relayTorInput')?.value.trim() || ''
};
try {
await callArchyRPC('bitcoin.relay-update-settings', payload);
if (statusEl) statusEl.textContent = 'Relay settings saved.';
await loadRelayAccess();
} catch (error) {
if (statusEl) statusEl.textContent = `Save failed: ${error.message}`;
}
}
async function requestPeerRelay() {
const statusEl = document.getElementById('relayStatusMessage');
const peer = document.getElementById('relayPeerSelect')?.value;
if (!peer) {
if (statusEl) statusEl.textContent = 'Choose a trusted node first.';
return;
}
try {
await callArchyRPC('bitcoin.relay-request-peer', {
peer_pubkey: peer,
message: document.getElementById('relayRequestMessage')?.value || ''
});
if (statusEl) statusEl.textContent = 'Relay access request sent.';
await loadRelayAccess();
} catch (error) {
if (statusEl) statusEl.textContent = `Request failed: ${error.message}`;
}
}
async function approveRelayRequest(id) {
await updateRelayRequest('bitcoin.relay-approve-request', id);
}
async function rejectRelayRequest(id) {
await updateRelayRequest('bitcoin.relay-reject-request', id);
}
async function updateRelayRequest(method, id) {
const statusEl = document.getElementById('relayStatusMessage');
try {
await callArchyRPC(method, { id });
if (statusEl) statusEl.textContent = 'Relay request updated.';
await loadRelayAccess();
} catch (error) {
if (statusEl) statusEl.textContent = `Update failed: ${error.message}`;
}
}
async function createRelayTorService() {
const statusEl = document.getElementById('relayStatusMessage');
try {
await callArchyRPC('bitcoin.relay-create-tor-service');
if (statusEl) statusEl.textContent = 'Tor service requested.';
await loadRelayAccess();
} catch (error) {
if (statusEl) statusEl.textContent = `Tor setup failed: ${error.message}`;
}
}
// Implementation branding — detected from getnetworkinfo.subversion.
// Bitcoin Knots identifies as "/Satoshi:<ver>/Knots:<date>/", Bitcoin Core as "/Satoshi:<ver>/".
let brandingApplied = false;
function applyImplBranding(subversion) {
if (brandingApplied) return;
if (!subversion) return;
const isKnots = /Knots/i.test(subversion);
const name = isKnots ? 'Bitcoin Knots' : 'Bitcoin Core';
const tagline = isKnots
? 'Enhanced Bitcoin node implementation'
: 'Reference Bitcoin node implementation';
const icon = isKnots
? '/assets/img/app-icons/bitcoin-knots.webp'
: '/assets/img/app-icons/bitcoin-core.svg';
const pageTitle = document.getElementById('pageTitle');
const implName = document.getElementById('implName');
const implTagline = document.getElementById('implTagline');
const implLogo = document.getElementById('implLogo');
if (pageTitle) pageTitle.textContent = `${name} - Archipelago`;
if (implName) implName.textContent = name;
if (implTagline) implTagline.textContent = tagline;
if (implLogo) { implLogo.src = icon; implLogo.alt = name; }
brandingApplied = true;
}
// Track last block count for animations
let lastBlockCount = 0;
let consecutiveRpcFailures = 0;
let lastSuccessfulUpdateAt = 0;
function formatPercent(value) {
if (!Number.isFinite(value) || value <= 0) return '0.00';
if (value < 0.01) return '<0.01';
return value.toFixed(2);
}
function formatBytes(bytes) {
if (!Number.isFinite(bytes) || bytes <= 0) return null;
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let value = bytes;
let unit = 0;
while (value >= 1000 && unit < units.length - 1) {
value /= 1000;
unit += 1;
}
return `${value.toFixed(unit >= 3 ? 1 : 0)} ${units[unit]}`;
}
// Update blockchain info
async function updateBlockchainInfo() {
console.log('[Bitcoin UI] updateBlockchainInfo() called');
try {
const status = await fetchBitcoinStatus();
const blockchainInfo = status.blockchain_info;
console.log('[Bitcoin UI] blockchainInfo:', blockchainInfo);
if (!blockchainInfo) {
console.error('[Bitcoin UI] No blockchain info received');
consecutiveRpcFailures += 1;
const syncStatusText = document.getElementById('syncStatusText');
const syncIcon = document.getElementById('syncIcon');
if (syncStatusText) {
if (status.stale) {
syncStatusText.textContent = status.error || 'Bitcoin node is reconnecting... showing last known values';
syncStatusText.className = 'text-yellow-300 text-sm font-medium';
} else if (consecutiveRpcFailures < 6) {
syncStatusText.textContent = status.error || 'Bitcoin node is starting or busy syncing...';
syncStatusText.className = 'text-yellow-300 text-sm font-medium';
} else {
syncStatusText.textContent = status.error || 'Bitcoin node is still syncing; retrying automatically...';
syncStatusText.className = 'text-yellow-300 text-sm font-medium';
}
}
if (syncIcon) {
syncIcon.classList.add('animate-spin-slow');
syncIcon.classList.remove('text-green-500');
}
return;
}
consecutiveRpcFailures = 0;
lastSuccessfulUpdateAt = Date.now();
const networkInfo = status.network_info;
applyImplBranding(networkInfo && networkInfo.subversion);
// Update network mode
const chain = blockchainInfo.chain || 'unknown';
const networkType = document.getElementById('networkType');
let networkShort = '';
if (chain === 'regtest') {
networkShort = 'Regtest';
} else if (chain === 'test') {
networkShort = 'Testnet';
} else if (chain === 'main') {
networkShort = 'Mainnet';
} else {
networkShort = chain;
}
if (networkType) networkType.textContent = networkShort;
// Mirror to Settings modal — Network Mode
const settingsNetworkMode = document.getElementById('settingsNetworkMode');
if (settingsNetworkMode) {
const labels = { main: 'Mainnet', test: 'Testnet', signet: 'Signet', regtest: 'Regtest (Development)' };
settingsNetworkMode.textContent = labels[chain] || networkShort;
}
// Update storage mode (pruned vs full archive)
const storageMode = document.getElementById('storageMode');
if (storageMode) {
const sizeGb = blockchainInfo.size_on_disk
? (blockchainInfo.size_on_disk / 1e9).toFixed(1) + ' GB'
: null;
if (blockchainInfo.pruned) {
storageMode.textContent = sizeGb ? `Pruned · ${sizeGb}` : 'Pruned';
storageMode.className = 'text-sm font-medium text-amber-300';
} else {
storageMode.textContent = sizeGb ? `Full Archive · ${sizeGb}` : 'Full Archive';
storageMode.className = 'text-sm font-medium text-emerald-300';
}
}
// Mirror to Settings modal — Storage Mode
const settingsStorageMode = document.getElementById('settingsStorageMode');
if (settingsStorageMode) {
if (blockchainInfo.pruned) {
const heightNote = blockchainInfo.prune_height != null
? ` (keeping from block ${blockchainInfo.prune_height.toLocaleString()})` : '';
settingsStorageMode.textContent = `Pruned${heightNote}`;
} else {
settingsStorageMode.textContent = 'Full archive (no pruning)';
}
}
// Populate Settings — Transaction Index, ZMQ, RPC (fire-and-forget)
const txIndexEl = document.getElementById('settingsTxIndex');
if (txIndexEl) {
const idx = status.index_info;
if (idx && typeof idx === 'object') {
const names = Object.keys(idx);
txIndexEl.textContent = names.length
? `Enabled: ${names.join(', ')}`
: 'Disabled';
} else {
txIndexEl.textContent = 'Unavailable while node starts';
}
}
const zmqEl = document.getElementById('settingsZmq');
if (zmqEl) {
const zmq = status.zmq_notifications;
if (Array.isArray(zmq) && zmq.length) {
zmqEl.textContent = zmq.map(z => `${z.type}@${z.address}`).join('; ');
} else if (Array.isArray(zmq)) {
zmqEl.textContent = 'Not enabled';
} else {
zmqEl.textContent = 'Unavailable while node starts';
}
}
const rpcEl = document.getElementById('settingsRpc');
if (rpcEl) {
const port = chain === 'main' ? 8332 : (chain === 'test' ? 18332 : (chain === 'signet' ? 38332 : 18443));
rpcEl.textContent = status.stale
? `Reconnecting on port ${port}`
: `Reachable on port ${port}`;
}
// Update sync status
const blocks = blockchainInfo.blocks || 0;
const headers = blockchainInfo.headers || 0;
const verificationProgress = blockchainInfo.verificationprogress || 0;
const initialBlockDownload = blockchainInfo.initialblockdownload === true;
const isSynced = headers > 0 && blocks >= headers - 1 && !initialBlockDownload;
const diskSize = formatBytes(blockchainInfo.size_on_disk || 0);
const appearsToBeReindexing = initialBlockDownload && blocks === 0 && headers > 0 && (blockchainInfo.size_on_disk || 0) > 1024 * 1024 * 1024;
// Calculate actual sync percentage based on blocks/headers
const actualSyncValue = headers > 0 ? (blocks / headers) * 100 : 0;
const actualSyncPercentage = formatPercent(actualSyncValue);
const progressWidth = Math.max(0, Math.min(100, actualSyncValue));
const verificationPercentage = formatPercent(verificationProgress * 100);
// Animate block count if it changed
const currentHeightElem = document.getElementById('currentHeight');
if (blocks !== lastBlockCount && lastBlockCount > 0) {
currentHeightElem.classList.add('number-update');
setTimeout(() => currentHeightElem.classList.remove('number-update'), 500);
}
lastBlockCount = blocks;
currentHeightElem.textContent = blocks.toLocaleString();
document.getElementById('networkHeight').textContent = headers.toLocaleString();
document.getElementById('headers').textContent = headers.toLocaleString();
document.getElementById('verificationProgress').textContent = `${verificationPercentage}%`;
document.getElementById('syncPercentage').textContent = `${actualSyncPercentage}%`;
document.getElementById('currentBlock').textContent = appearsToBeReindexing
? 'Reindexing from disk'
: `Block ${blocks.toLocaleString()}`;
document.getElementById('syncProgressBar').style.width = `${progressWidth}%`;
// Update sync status text and icon
const syncStatusText = document.getElementById('syncStatusText');
const syncIcon = document.getElementById('syncIcon');
if (appearsToBeReindexing) {
syncStatusText.textContent = `Reindexing local block files${diskSize ? ` (${diskSize} on disk)` : ''}`;
syncStatusText.className = 'text-orange-400 text-sm font-medium';
if (syncIcon) {
syncIcon.classList.add('animate-spin-slow');
syncIcon.classList.remove('text-green-500');
}
} else if (isSynced) {
syncStatusText.textContent = status.stale
? 'Bitcoin node is reconnecting... showing last known synchronized state'
: '✓ Fully synchronized with the network';
syncStatusText.className = status.stale ? 'text-yellow-300 text-sm font-medium' : 'text-green-400 text-sm font-medium';
// Stop spinning when synced
if (syncIcon) {
syncIcon.classList.remove('animate-spin-slow');
syncIcon.classList.add('text-green-500');
}
} else {
const remaining = headers - blocks;
syncStatusText.textContent = status.stale
? 'Bitcoin node is reconnecting... showing last known sync state'
: initialBlockDownload
? `Initial block download... ${remaining.toLocaleString()} blocks remaining`
: `Syncing... ${remaining.toLocaleString()} blocks remaining`;
syncStatusText.className = status.stale ? 'text-yellow-300 text-sm font-medium' : 'text-orange-400 text-sm font-medium';
// Keep spinning while syncing
if (syncIcon) {
syncIcon.classList.add('animate-spin-slow');
syncIcon.classList.remove('text-green-500');
}
}
// Update block height in quick actions (removed section)
// document.getElementById('blockHeight').textContent = blocks.toLocaleString();
// Update version
if (networkInfo && networkInfo.version) {
const version = networkInfo.version;
const versionStr = `v${Math.floor(version / 10000)}.${Math.floor((version % 10000) / 100)}.${version % 100}`;
const versionElem = document.getElementById('nodeVersion');
if (versionElem) versionElem.textContent = versionStr;
}
} catch (error) {
console.error('Failed to update blockchain info:', error);
consecutiveRpcFailures += 1;
const syncStatusText = document.getElementById('syncStatusText');
if (syncStatusText) {
const hasRecentData = lastSuccessfulUpdateAt > 0 && Date.now() - lastSuccessfulUpdateAt < 120000;
syncStatusText.textContent = hasRecentData
? 'Bitcoin status bridge is retrying... keeping last known values'
: 'Bitcoin status bridge is starting...';
syncStatusText.className = 'text-yellow-300 text-sm font-medium';
}
}
}
// Initial update
console.log('[Bitcoin UI] Starting initial blockchain info update...');
updateBlockchainInfo();
loadRelayAccess();
// Update every 5 seconds
console.log('[Bitcoin UI] Setting up 5-second update interval');
setInterval(updateBlockchainInfo, 5000);
setInterval(loadRelayAccess, 15000);
function copyRPCInfo() {
const info = `RPC Host: ${window.location.hostname}:8332\nRPC User: archipelago\nRPC Password: archipelago123\nRPC Endpoint: ${RPC_ENDPOINT}`;
navigator.clipboard.writeText(info).then(() => {
alert('RPC 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 openLogs() {
document.getElementById('logsModal').classList.remove('hidden');
document.getElementById('logsModal').classList.add('flex');
loadLogs();
}
function closeLogs() {
document.getElementById('logsModal').classList.add('hidden');
document.getElementById('logsModal').classList.remove('flex');
}
async function loadLogs() {
const logsContent = document.getElementById('logsContent');
logsContent.textContent = 'Loading logs from node...';
try {
const networkInfo = await callRPC('getnetworkinfo');
const blockchainInfo = await callRPC('getblockchaininfo');
const peerInfo = await callRPC('getpeerinfo');
if (networkInfo && blockchainInfo) {
applyImplBranding(networkInfo.subversion);
const implLabel = /Knots/i.test(networkInfo.subversion || '') ? 'Bitcoin Knots' : 'Bitcoin Core';
logsContent.textContent = `${implLabel} version ${networkInfo.subversion || 'unknown'}
Network: ${blockchainInfo.chain}
Blocks: ${blockchainInfo.blocks}
Headers: ${blockchainInfo.headers}
Verification Progress: ${(blockchainInfo.verificationprogress * 100).toFixed(2)}%
Connected Peers: ${peerInfo ? peerInfo.length : 0}
Difficulty: ${blockchainInfo.difficulty}
Chain Work: ${blockchainInfo.chainwork || 'N/A'}
Node is running and accepting connections.
RPC server active on port 8332`;
} else {
logsContent.textContent = 'Unable to fetch node logs. Please check your RPC connection.';
}
} catch (error) {
logsContent.textContent = `Error loading logs: ${error.message}`;
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeSettings();
closeLogs();
}
});
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeSettings();
closeLogs();
}
});
});
</script>
</body>
</html>