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 : 100 vh ;
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 : 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" >
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 >