@@ -0,0 +1,818 @@
<!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 > Bitcoin Knots - 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 : 100 vh ;
color : white ;
overflow-x : hidden ;
}
/* Background - Web5 style */
. bg-perspective-container {
position : fixed ;
inset : 0 ;
z-index : -10 ;
perspective : 1000 px ;
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.45 s 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 ( 24 px ) ;
-webkit- backdrop-filter : blur ( 24 px ) ;
box-shadow :
0 8 px 24 px rgba ( 0 , 0 , 0 , 0.45 ) ,
inset 0 1 px 0 rgba ( 255 , 255 , 255 , 0.22 ) ;
border-radius : 1 rem ;
overflow-x : hidden ;
overflow-y : visible ;
border : none ;
}
. glass-card :: before {
content : '' ;
position : absolute ;
inset : 0 ;
border-radius : inherit ;
padding : 2 px ;
background : linear-gradient ( 135 deg , 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 ( 18 px ) ;
-webkit- backdrop-filter : blur ( 18 px ) ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.18 ) ;
color : rgba ( 255 , 255 , 255 , 0.9 ) ;
transition : all 0.3 s 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 ( 135 deg , rgba ( 255 , 255 , 255 , 0.15 ) 0 % , rgba ( 0 , 0 , 0 , 0.8 ) 100 % ) ;
backdrop-filter : blur ( 10 px ) ;
-webkit- backdrop-filter : blur ( 10 px ) ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.2 ) ;
color : rgba ( 255 , 255 , 255 , 0.95 ) ;
transition : all 0.3 s ease ;
}
. gradient-button : hover {
background : linear-gradient ( 135 deg , 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 8 px 24 px rgba ( 0 , 0 , 0 , 0.6 ) ;
transform : translateY ( -1 px ) ;
}
. gradient-button : active {
transform : translateY ( 1 px ) ;
}
/* Interactive card - Archipelago standard (display only, no hover) */
. info-card {
position : relative ;
background : rgba ( 0 , 0 , 0 , 0.60 ) ;
backdrop-filter : blur ( 24 px ) ;
-webkit- backdrop-filter : blur ( 24 px ) ;
box-shadow :
0 8 px 24 px rgba ( 0 , 0 , 0 , 0.45 ) ,
inset 0 1 px 0 rgba ( 255 , 255 , 255 , 0.22 ) ;
border-radius : 16 px ;
padding : 12 px ;
border : none ;
}
. info-card :: before {
content : '' ;
position : absolute ;
inset : 0 ;
border-radius : inherit ;
padding : 2 px ;
background : linear-gradient ( 135 deg , 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 ( 24 px ) ;
-webkit- backdrop-filter : blur ( 24 px ) ;
box-shadow :
0 8 px 24 px rgba ( 0 , 0 , 0 , 0.45 ) ,
inset 0 1 px 0 rgba ( 255 , 255 , 255 , 0.22 ) ;
border-radius : 16 px ;
padding : 12 px ;
transition : all 0.3 s 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 : 2 px ;
background : linear-gradient ( 135 deg , 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.3 s ease ;
}
. info-card-button : hover {
transform : translateY ( -2 px ) ;
background : rgba ( 0 , 0 , 0 , 0.35 ) ;
box-shadow :
0 12 px 32 px rgba ( 0 , 0 , 0 , 0.6 ) ,
inset 0 1 px 0 rgba ( 255 , 255 , 255 , 0.25 ) ;
color : rgba ( 255 , 255 , 255 , 1 ) ;
}
. info-card-button : hover :: before {
background : linear-gradient ( 135 deg , rgba ( 255 , 255 , 255 , 0.3 ) , transparent ) ;
}
. info-card-button : active {
transform : translateY ( 1 px ) ;
}
/* Container */
. container {
max-width : 1400 px ;
margin : 0 auto ;
padding : 2 rem ;
padding-bottom : 4 rem ;
}
/* Logo gradient border */
. logo-gradient-border {
position : relative ;
border-radius : 16 px ;
padding : 3 px ;
background : linear-gradient ( 135 deg , rgba ( 255 , 255 , 255 , 0.6 ) 0 % , rgba ( 0 , 0 , 0 , 0.8 ) 100 % ) ;
box-shadow : 0 8 px 24 px rgba ( 0 , 0 , 0 , 0.5 ) ;
display : inline-block ;
}
. logo-gradient-border :: after {
content : '' ;
position : absolute ;
inset : 3 px ;
border-radius : 13 px ;
background : #fff ;
z-index : 0 ;
}
. logo-gradient-border img {
border-radius : 13 px ;
display : block ;
position : relative ;
z-index : 1 ;
width : 64 px ;
height : 64 px ;
}
/* Ping animation for status dots */
@ keyframes ping {
75 % , 100 % {
transform : scale ( 2 ) ;
opacity : 0 ;
}
}
. animate-ping {
animation : ping 1 s cubic-bezier ( 0 , 0 , 0.2 , 1 ) infinite ;
}
/* Pulsing glow for progress bar */
@ keyframes progressGlow {
0 % , 100 % {
box-shadow : 0 0 10 px rgba ( 251 , 146 , 60 , 0.5 ) ,
0 0 20 px rgba ( 251 , 146 , 60 , 0.3 ) ,
0 0 30 px rgba ( 251 , 146 , 60 , 0.1 ) ;
}
50 % {
box-shadow : 0 0 20 px rgba ( 251 , 146 , 60 , 0.8 ) ,
0 0 30 px rgba ( 251 , 146 , 60 , 0.5 ) ,
0 0 40 px rgba ( 251 , 146 , 60 , 0.3 ) ;
}
}
. progress-glow {
animation : progressGlow 2 s ease-in-out infinite ;
}
/* Spinning animation */
@ keyframes spin {
from {
transform : rotate ( 0 deg ) ;
}
to {
transform : rotate ( 360 deg ) ;
}
}
. animate-spin-slow {
animation : spin 3 s linear infinite ;
}
/* Shimmer effect */
@ keyframes shimmer {
0 % {
background-position : -1000 px 0 ;
}
100 % {
background-position : 1000 px 0 ;
}
}
. shimmer {
background : linear-gradient (
90 deg ,
rgba ( 255 , 255 , 255 , 0 ) 0 % ,
rgba ( 255 , 255 , 255 , 0.1 ) 50 % ,
rgba ( 255 , 255 , 255 , 0 ) 100 %
) ;
background-size : 1000 px 100 % ;
animation : shimmer 3 s 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.5 s 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
src = "/assets/img/app-icons/bitcoin-knots.webp"
alt = "Bitcoin Knots"
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 class = "text-3xl font-bold text-white mb-2" > Bitcoin Knots< / h1 >
< p class = "text-white/70" > Enhanced Bitcoin node 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 >
< 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 >
<!-- 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" > Regtest (Development)< / 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" > Enabled (txindex=1)< / 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" > Block & TX notifications enabled< / 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" > Enabled on 0.0.0.0:18443< / 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/' ;
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 ;
}
}
// Track last block count for animations
let lastBlockCount = 0 ;
// Update blockchain info
async function updateBlockchainInfo ( ) {
console . log ( '[Bitcoin UI] updateBlockchainInfo() called' ) ;
try {
const blockchainInfo = await callRPC ( 'getblockchaininfo' ) ;
console . log ( '[Bitcoin UI] blockchainInfo:' , blockchainInfo ) ;
if ( ! blockchainInfo ) {
console . error ( '[Bitcoin UI] No blockchain info received' ) ;
document . getElementById ( 'syncStatusText' ) . textContent = 'Unable to connect to Bitcoin node' ;
document . getElementById ( 'syncStatusText' ) . className = 'text-red-400 text-sm' ;
return ;
}
const networkInfo = await callRPC ( 'getnetworkinfo' ) ;
// 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 ;
// Update sync status
const blocks = blockchainInfo . blocks || 0 ;
const headers = blockchainInfo . headers || 0 ;
const verificationProgress = blockchainInfo . verificationprogress || 0 ;
const isSynced = blocks >= headers - 1 ;
// Calculate actual sync percentage based on blocks/headers
const actualSyncPercentage = headers > 0 ? ( ( blocks / headers ) * 100 ) . toFixed ( 2 ) : '0.00' ;
const verificationPercentage = ( verificationProgress * 100 ) . toFixed ( 2 ) ;
// 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 = ` Block ${ blocks . toLocaleString ( ) } ` ;
document . getElementById ( 'syncProgressBar' ) . style . width = ` ${ actualSyncPercentage } % ` ;
// Update sync status text and icon
const syncStatusText = document . getElementById ( 'syncStatusText' ) ;
const syncIcon = document . getElementById ( 'syncIcon' ) ;
if ( isSynced ) {
syncStatusText . textContent = '✓ Fully synchronized with the network' ;
syncStatusText . className = '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 = ` Syncing... ${ remaining . toLocaleString ( ) } blocks remaining ` ;
syncStatusText . className = '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 ) ;
document . getElementById ( 'syncStatusText' ) . textContent = 'Unable to fetch blockchain data' ;
document . getElementById ( 'syncStatusText' ) . className = 'text-red-400 text-sm' ;
}
}
// Initial update
console . log ( '[Bitcoin UI] Starting initial blockchain info update...' ) ;
updateBlockchainInfo ( ) ;
// Update every 5 seconds
console . log ( '[Bitcoin UI] Setting up 5-second update interval' ) ;
setInterval ( updateBlockchainInfo , 5000 ) ;
function copyRPCInfo ( ) {
const info = ` RPC Host: ${ window . location . hostname } :8332 \n RPC User: archipelago \n RPC Password: archipelago123 \n RPC 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 ) {
logsContent . textContent = ` Bitcoin Knots 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 >