2026-03-22 03:30:21 +00:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< meta http-equiv = "Cache-Control" content = "no-cache, no-store, must-revalidate" >
< meta http-equiv = "Pragma" content = "no-cache" >
< meta http-equiv = "Expires" content = "0" >
2026-04-22 11:03:47 -04:00
< title id = "pageTitle" > Bitcoin Node - Archipelago</ title >
2026-07-15 09:25:38 +01:00
<!-- Relative: the shell is served at / in the container but under
/app/bitcoin-ui/ in the public demo — absolute paths 404 there. -->
< link rel = "stylesheet" href = "tailwind.css" >
2026-03-22 03:30:21 +00:00
< 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;
2026-06-11 00:24:32 -04:00
background: #000;
2026-03-22 03:30:21 +00:00
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" >
2026-04-22 11:03:47 -04:00
< img
id = "implLogo"
2026-07-15 09:25:38 +01:00
src = "assets/img/app-icons/bitcoin-knots.webp"
2026-04-22 11:03:47 -04:00
alt = "Bitcoin Node"
2026-03-22 03:30:21 +00:00
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" >
2026-04-22 11:03:47 -04:00
< 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 >
2026-03-22 03:30:21 +00:00
</ 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 >
2026-04-22 11:03:47 -04:00
< 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()"
2026-03-22 03:30:21 +00:00
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 >
2026-06-11 00:24:32 -04:00
< 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 >
2026-03-22 03:30:21 +00:00
</ 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 >
2026-04-22 11:03:47 -04:00
< 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 >
2026-03-22 03:30:21 +00:00
</ div >
< div class = "p-3 bg-white/5 rounded-lg" >
< div class = "font-semibold text-white mb-1" > Transaction Index</ div >
2026-04-22 11:03:47 -04:00
< div class = "text-white/70 text-sm" id = "settingsTxIndex" > Loading…</ div >
2026-03-22 03:30:21 +00:00
</ div >
< div class = "p-3 bg-white/5 rounded-lg" >
< div class = "font-semibold text-white mb-1" > ZMQ Publishing</ div >
2026-04-22 11:03:47 -04:00
< div class = "text-white/70 text-sm" id = "settingsZmq" > Loading…</ div >
2026-03-22 03:30:21 +00:00
</ div >
< div class = "p-3 bg-white/5 rounded-lg" >
< div class = "font-semibold text-white mb-1" > RPC Access</ div >
2026-04-22 11:03:47 -04:00
< div class = "text-white/70 text-sm" id = "settingsRpc" > Loading…</ div >
2026-03-22 03:30:21 +00:00
</ 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
2026-04-30 16:29:56 -04:00
const RPC_ENDPOINT = 'bitcoin-rpc/' ;
const STATUS_ENDPOINT = 'bitcoin-status' ;
2026-06-11 00:24:32 -04:00
const ARCHY_RPC_ENDPOINT = 'rpc/v1' ;
2026-03-22 03:30:21 +00:00
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 ;
}
}
2026-04-30 16:29:56 -04:00
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 ();
}
2026-06-19 05:03:18 -04:00
// Snapshot age in ms. Prefer the server-computed age_ms (single clock, no
// skew). Fall back to the old browser-vs-server subtraction only for an
// older backend that doesn't send age_ms. Mixing clocks was why the
// "reconnecting…" banner could stick on nodes whose clock drifted.
function snapshotAgeMs ( status ) {
if ( typeof status . age_ms === 'number' ) return status . age_ms ;
return status . updated_at_ms ? Date . now () - status . updated_at_ms : Number . POSITIVE_INFINITY ;
}
2026-06-11 00:24:32 -04:00
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 => ({
'&' : '&' ,
'<' : '<' ,
'>' : '>' ,
'"' : '"' ,
"'" : '''
}[ char ]));
}
function setText ( id , value , fallback = 'Not configured' ) {
const el = document . getElementById ( id );
if ( el ) el . textContent = value || fallback ;
}
2026-06-11 02:32:10 -04:00
function setTextIfPresent ( id , value ) {
const el = document . getElementById ( id );
if ( el ) el . textContent = value ;
return el ;
}
function setWidthIfPresent ( id , value ) {
const el = document . getElementById ( id );
if ( el ) el . style . width = value ;
return el ;
}
2026-06-11 00:24:32 -04:00
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 } ` ;
}
}
2026-04-22 11:03:47 -04:00
// 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
2026-07-15 09:25:38 +01:00
? 'assets/img/app-icons/bitcoin-knots.webp'
: 'assets/img/app-icons/bitcoin-core.svg' ;
2026-04-22 11:03:47 -04:00
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 ;
}
2026-03-22 03:30:21 +00:00
// Track last block count for animations
let lastBlockCount = 0 ;
2026-04-30 16:29:56 -04:00
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 ] } ` ;
}
2026-03-22 03:30:21 +00:00
// Update blockchain info
async function updateBlockchainInfo () {
console . log ( '[Bitcoin UI] updateBlockchainInfo() called' );
try {
2026-04-30 16:29:56 -04:00
const status = await fetchBitcoinStatus ();
const blockchainInfo = status . blockchain_info ;
2026-03-22 03:30:21 +00:00
console . log ( '[Bitcoin UI] blockchainInfo:' , blockchainInfo );
if ( ! blockchainInfo ) {
console . error ( '[Bitcoin UI] No blockchain info received' );
2026-04-30 16:29:56 -04:00
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 ) {
2026-06-11 00:24:32 -04:00
syncStatusText . textContent = status . error || 'Bitcoin node is starting or busy syncing...' ;
2026-04-30 16:29:56 -04:00
syncStatusText . className = 'text-yellow-300 text-sm font-medium' ;
} else {
2026-06-11 00:24:32 -04:00
syncStatusText . textContent = status . error || 'Bitcoin node is still syncing; retrying automatically...' ;
syncStatusText . className = 'text-yellow-300 text-sm font-medium' ;
2026-04-30 16:29:56 -04:00
}
}
if ( syncIcon ) {
syncIcon . classList . add ( 'animate-spin-slow' );
syncIcon . classList . remove ( 'text-green-500' );
}
2026-03-22 03:30:21 +00:00
return ;
}
2026-04-30 16:29:56 -04:00
consecutiveRpcFailures = 0 ;
lastSuccessfulUpdateAt = Date . now ();
2026-03-22 03:30:21 +00:00
2026-04-30 16:29:56 -04:00
const networkInfo = status . network_info ;
2026-04-22 11:03:47 -04:00
applyImplBranding ( networkInfo && networkInfo . subversion );
2026-03-22 03:30:21 +00:00
// 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 ;
2026-04-22 11:03:47 -04:00
// 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)
2026-04-30 16:29:56 -04:00
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' ;
2026-04-22 11:03:47 -04:00
}
2026-04-30 16:29:56 -04:00
}
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' ;
2026-04-22 11:03:47 -04:00
}
2026-04-30 16:29:56 -04:00
}
const rpcEl = document . getElementById ( 'settingsRpc' );
if ( rpcEl ) {
const port = chain === 'main' ? 8332 : ( chain === 'test' ? 18332 : ( chain === 'signet' ? 38332 : 18443 ));
2026-06-19 05:03:18 -04:00
const statusAgeMs = snapshotAgeMs ( status );
2026-06-11 02:32:10 -04:00
const displayStale = status . stale === true && statusAgeMs > 30000 ;
rpcEl . textContent = displayStale
2026-04-30 16:29:56 -04:00
? `Reconnecting on port ${ port } `
: `Reachable on port ${ port } ` ;
}
2026-04-22 11:03:47 -04:00
2026-03-22 03:30:21 +00:00
// Update sync status
const blocks = blockchainInfo . blocks || 0 ;
const headers = blockchainInfo . headers || 0 ;
const verificationProgress = blockchainInfo . verificationprogress || 0 ;
2026-04-30 16:29:56 -04:00
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 ;
2026-06-11 02:32:10 -04:00
const previousBlockCount = lastBlockCount ;
2026-06-19 05:03:18 -04:00
const statusAgeMs = snapshotAgeMs ( status );
2026-06-11 02:32:10 -04:00
const snapshotAdvanced = previousBlockCount > 0 && blocks > previousBlockCount ;
const displayStale = status . stale === true && ! snapshotAdvanced && statusAgeMs > 30000 ;
2026-03-22 03:30:21 +00:00
// Calculate actual sync percentage based on blocks/headers
2026-04-30 16:29:56 -04:00
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 );
2026-03-22 03:30:21 +00:00
// Animate block count if it changed
const currentHeightElem = document . getElementById ( 'currentHeight' );
2026-06-11 02:32:10 -04:00
if ( currentHeightElem && blocks !== lastBlockCount && lastBlockCount > 0 ) {
2026-03-22 03:30:21 +00:00
currentHeightElem . classList . add ( 'number-update' );
setTimeout (() => currentHeightElem . classList . remove ( 'number-update' ), 500 );
}
lastBlockCount = blocks ;
2026-06-11 02:32:10 -04:00
setTextIfPresent ( 'currentHeight' , blocks . toLocaleString ());
setTextIfPresent ( 'networkHeight' , headers . toLocaleString ());
setTextIfPresent ( 'headers' , headers . toLocaleString ());
setTextIfPresent ( 'verificationProgress' , ` ${ verificationPercentage } %` );
setTextIfPresent ( 'syncPercentage' , ` ${ actualSyncPercentage } %` );
setTextIfPresent ( 'currentBlock' , appearsToBeReindexing
2026-04-30 16:29:56 -04:00
? 'Reindexing from disk'
2026-06-11 02:32:10 -04:00
: `Block ${ blocks . toLocaleString () } ` );
setWidthIfPresent ( 'syncProgressBar' , ` ${ progressWidth } %` );
2026-03-22 03:30:21 +00:00
// Update sync status text and icon
const syncStatusText = document . getElementById ( 'syncStatusText' );
const syncIcon = document . getElementById ( 'syncIcon' );
2026-04-30 16:29:56 -04:00
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 ) {
2026-06-11 02:32:10 -04:00
syncStatusText . textContent = displayStale
2026-04-30 16:29:56 -04:00
? 'Bitcoin node is reconnecting... showing last known synchronized state'
: '✓ Fully synchronized with the network' ;
2026-06-11 02:32:10 -04:00
syncStatusText . className = displayStale ? 'text-yellow-300 text-sm font-medium' : 'text-green-400 text-sm font-medium' ;
2026-03-22 03:30:21 +00:00
// Stop spinning when synced
if ( syncIcon ) {
syncIcon . classList . remove ( 'animate-spin-slow' );
syncIcon . classList . add ( 'text-green-500' );
}
} else {
const remaining = headers - blocks ;
2026-06-11 02:32:10 -04:00
syncStatusText . textContent = displayStale
2026-04-30 16:29:56 -04:00
? 'Bitcoin node is reconnecting... showing last known sync state'
: initialBlockDownload
? `Initial block download... ${ remaining . toLocaleString () } blocks remaining`
: `Syncing... ${ remaining . toLocaleString () } blocks remaining` ;
2026-06-11 02:32:10 -04:00
syncStatusText . className = displayStale ? 'text-yellow-300 text-sm font-medium' : 'text-orange-400 text-sm font-medium' ;
2026-03-22 03:30:21 +00:00
// 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 );
2026-04-30 16:29:56 -04:00
consecutiveRpcFailures += 1 ;
const syncStatusText = document . getElementById ( 'syncStatusText' );
if ( syncStatusText ) {
const hasRecentData = lastSuccessfulUpdateAt > 0 && Date . now () - lastSuccessfulUpdateAt < 120000 ;
syncStatusText . textContent = hasRecentData
2026-06-11 00:24:32 -04:00
? 'Bitcoin status bridge is retrying... keeping last known values'
: 'Bitcoin status bridge is starting...' ;
2026-04-30 16:29:56 -04:00
syncStatusText . className = 'text-yellow-300 text-sm font-medium' ;
}
2026-03-22 03:30:21 +00:00
}
}
// Initial update
console . log ( '[Bitcoin UI] Starting initial blockchain info update...' );
updateBlockchainInfo ();
2026-06-11 00:24:32 -04:00
loadRelayAccess ();
2026-03-22 03:30:21 +00:00
// Update every 5 seconds
console . log ( '[Bitcoin UI] Setting up 5-second update interval' );
setInterval ( updateBlockchainInfo , 5000 );
2026-06-11 00:24:32 -04:00
setInterval ( loadRelayAccess , 15000 );
2026-03-22 03:30:21 +00:00
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 ) {
2026-04-22 11:03:47 -04:00
applyImplBranding ( networkInfo . subversion );
const implLabel = /Knots/i . test ( networkInfo . subversion || '' ) ? 'Bitcoin Knots' : 'Bitcoin Core' ;
logsContent . textContent = ` ${ implLabel } version ${ networkInfo . subversion || 'unknown' }
2026-03-22 03:30:21 +00:00
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 >