2026-01-27 23:57:29 +00:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
2026-03-16 15:46:11 +00:00
< meta http-equiv = "Cache-Control" content = "no-cache, no-store, must-revalidate" >
2026-01-28 00:12:11 +00:00
< title > LND - Archipelago</ title >
2026-01-27 23:57:29 +00:00
< style >
2026-03-16 15:46:11 +00:00
* { margin : 0 ; padding : 0 ; box-sizing : border-box ; }
body { font-family : - apple-system , BlinkMacSystemFont , 'Segoe UI' , Roboto , sans-serif ; min-height : 100 vh ; color : white ; overflow-x : hidden ; }
. bg-layer { position : fixed ; inset : 0 ; z-index : -10 ; background : linear-gradient ( 135 deg , rgba ( 0 , 0 , 0 , 0.9 ) 0 % , rgba ( 30 , 30 , 50 , 0.95 ) 100 % ); }
. overlay { position : fixed ; inset : 0 ; background : rgba ( 0 , 0 , 0 , 0.8 ); z-index : -5 ; }
. glass-card { background : rgba ( 0 , 0 , 0 , 0.6 ); backdrop-filter : blur ( 24 px ); -webkit- backdrop-filter : blur ( 24 px ); border-radius : 1 rem ; border : 1 px solid rgba ( 255 , 255 , 255 , 0.18 ); }
. info-card { background : rgba ( 0 , 0 , 0 , 0.6 ); backdrop-filter : blur ( 24 px ); border-radius : 16 px ; padding : 12 px ; border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ); }
. info-card-button { background : rgba ( 0 , 0 , 0 , 0.6 ); backdrop-filter : blur ( 24 px ); border-radius : 16 px ; padding : 12 px ; border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ); cursor : pointer ; color : rgba ( 255 , 255 , 255 , 0.9 ); transition : all 0.3 s ease ; }
. info-card-button : hover { transform : translateY ( -2 px ); background : rgba ( 255 , 255 , 255 , 0.08 ); }
. info-card-button : active { transform : translateY ( 1 px ); }
. 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 ; cursor : pointer ; }
. glass-button : hover { color : white ; background-color : rgba ( 0 , 0 , 0 , 0.7 ); }
. glass-button : disabled { opacity : 0.5 ; cursor : not-allowed ; }
. container { max-width : 56 rem ; margin : 0 auto ; padding : 2 rem ; padding-bottom : 4 rem ; }
. flex { display : flex ; }
. flex-col { flex-direction : column ; }
. items-center { align-items : center ; }
. items-start { align-items : start ; }
. justify-between { justify-content : space-between ; }
. gap-2 { gap : 0.5 rem ; }
. gap-3 { gap : 0.75 rem ; }
. gap-4 { gap : 1 rem ; }
. gap-6 { gap : 1.5 rem ; }
. flex-1 { flex : 1 ; }
. flex-shrink-0 { flex-shrink : 0 ; }
. min-w-0 { min-width : 0 ; }
. mb-1 { margin-bottom : 0.25 rem ; }
. mb-2 { margin-bottom : 0.5 rem ; }
. mb-3 { margin-bottom : 0.75 rem ; }
. mb-4 { margin-bottom : 1 rem ; }
. mb-6 { margin-bottom : 1.5 rem ; }
. mb-8 { margin-bottom : 2 rem ; }
. mt-2 { margin-top : 0.5 rem ; }
. mt-4 { margin-top : 1 rem ; }
. p-2 { padding : 0.5 rem ; }
. p-3 { padding : 0.75 rem ; }
. p-4 { padding : 1 rem ; }
. p-6 { padding : 1.5 rem ; }
. px-3 { padding-left : 0.75 rem ; padding-right : 0.75 rem ; }
. px-4 { padding-left : 1 rem ; padding-right : 1 rem ; }
. px-6 { padding-left : 1.5 rem ; padding-right : 1.5 rem ; }
. py-1-5 { padding-top : 0.375 rem ; padding-bottom : 0.375 rem ; }
. py-2 { padding-top : 0.5 rem ; padding-bottom : 0.5 rem ; }
. py-2-5 { padding-top : 0.625 rem ; padding-bottom : 0.625 rem ; }
. py-3 { padding-top : 0.75 rem ; padding-bottom : 0.75 rem ; }
. grid { display : grid ; }
. grid-cols-1 { grid-template-columns : repeat ( 1 , 1 fr ); }
. grid-cols-2 { grid-template-columns : repeat ( 2 , 1 fr ); }
. grid-cols-3 { grid-template-columns : repeat ( 3 , 1 fr ); }
. rounded-lg { border-radius : 0.5 rem ; }
. rounded-xl { border-radius : 0.75 rem ; }
. rounded-full { border-radius : 9999 px ; }
. overflow-hidden { overflow : hidden ; }
. transition-all { transition : all 0.3 s ease ; }
. text-xs { font-size : 0.75 rem ; }
. text-sm { font-size : 0.875 rem ; }
. text-lg { font-size : 1.125 rem ; }
. text-xl { font-size : 1.25 rem ; }
. text-2xl { font-size : 1.5 rem ; }
. text-3xl { font-size : 1.875 rem ; }
. font-bold { font-weight : 700 ; }
. font-semibold { font-weight : 600 ; }
. font-medium { font-weight : 500 ; }
. font-mono { font-family : monospace ; }
. uppercase { text-transform : uppercase ; }
. tracking-wide { letter-spacing : 0.05 em ; }
. truncate { white-space : nowrap ; overflow : hidden ; text-overflow : ellipsis ; }
. w-full { width : 100 % ; }
. text-white { color : white ; }
. text-white-90 { color : rgba ( 255 , 255 , 255 , 0.9 ); }
. text-white-80 { color : rgba ( 255 , 255 , 255 , 0.8 ); }
. text-white-70 { color : rgba ( 255 , 255 , 255 , 0.7 ); }
. text-white-60 { color : rgba ( 255 , 255 , 255 , 0.6 ); }
. text-white-50 { color : rgba ( 255 , 255 , 255 , 0.5 ); }
. text-white-45 { color : rgba ( 255 , 255 , 255 , 0.45 ); }
. text-white-40 { color : rgba ( 255 , 255 , 255 , 0.4 ); }
. text-green { color : #4ade80 ; }
. text-orange { color : #fb923c ; }
. text-red { color : #f87171 ; }
. bg-green { background : #4ade80 ; }
. bg-yellow { background : #facc15 ; }
. bg-red { background : #f87171 ; }
. icon-box { width : 4 rem ; height : 4 rem ; border-radius : 0.5 rem ; background : rgba ( 139 , 92 , 246 , 0.2 ); display : flex ; align-items : center ; justify-content : center ; }
. icon-box-sm { width : 3 rem ; height : 3 rem ; border-radius : 0.5 rem ; background : rgba ( 255 , 255 , 255 , 0.1 ); display : flex ; align-items : center ; justify-content : center ; }
. status-dot { width : 0.75 rem ; height : 0.75 rem ; border-radius : 9999 px ; }
. status-dot-sm { width : 0.625 rem ; height : 0.625 rem ; border-radius : 9999 px ; display : inline-block ; }
. relative { position : relative ; }
. version-text { font-size : 0.75 rem ; color : rgba ( 255 , 255 , 255 , 0.4 ); }
2026-01-27 23:57:29 +00:00
2026-03-16 15:46:11 +00:00
/* Logo border matching electrs icon-box but with LND purple accent */
. logo-border { width : 4 rem ; height : 4 rem ; border-radius : 0.5 rem ; background : rgba ( 139 , 92 , 246 , 0.2 ); display : flex ; align-items : center ; justify-content : center ; overflow : hidden ; }
. logo-border img { width : 3 rem ; height : 3 rem ; object-fit : contain ; }
2026-01-27 23:57:29 +00:00
2026-03-16 15:46:11 +00:00
/* Stat row inside info-card */
. stat-row { display : flex ; align-items : center ; justify-content : space-between ; padding : 0.75 rem ; background : rgba ( 255 , 255 , 255 , 0.05 ); border-radius : 0.5 rem ; }
2026-01-28 00:12:11 +00:00
2026-03-16 15:46:11 +00:00
/* Balance cards */
. balance-card { padding : 1 rem ; background : rgba ( 0 , 0 , 0 , 0.3 ); border-radius : 0.5 rem ; border : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ); }
2026-01-28 00:12:11 +00:00
2026-03-16 15:46:11 +00:00
/* Ping animation */
@ keyframes ping { 75 %, 100 % { transform : scale ( 2 ); opacity : 0 ; } }
. animate-ping { animation : ping 1 s cubic-bezier ( 0 , 0 , 0.2 , 1 ) infinite ; position : absolute ; inset : 0 ; }
2026-01-27 23:57:29 +00:00
2026-03-16 15:46:11 +00:00
/* Connection details */
. conn-select { width : 100 % ; padding : 0.75 rem 1 rem ; background : rgba ( 255 , 255 , 255 , 0.05 ); border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ); border-radius : 0.5 rem ; color : white ; font-size : 0.875 rem ; font-weight : 500 ; appearance : none ; cursor : pointer ; outline : none ; background-image : url ( 'data:image/svg+xml;utf8,<svg fill="white" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/></svg>' ); background-repeat : no-repeat ; background-position : right 12 px center ; background-size : 20 px ; }
. conn-select : focus { border-color : rgba ( 255 , 255 , 255 , 0.25 ); }
. conn-select option { background : #1a1a2e ; color : white ; }
. conn-layout { display : flex ; flex-direction : column ; gap : 1.5 rem ; }
@ media ( min-width : 640px ) { . conn-layout { flex-direction : row ; } }
. qr-box { flex-shrink : 0 ; width : 196 px ; height : 196 px ; background : white ; border-radius : 0.75 rem ; padding : 0.5 rem ; display : flex ; align-items : center ; justify-content : center ; }
. qr-box img { width : 100 % ; height : auto ; display : block ; image-rendering : pixelated ; }
2026-03-16 16:53:25 +00:00
. conn-fields { flex : 1 ; min-width : 0 ; display : flex ; flex-direction : column ; gap : 0.75 rem ; }
2026-03-16 15:46:11 +00:00
. field-label { font-size : 0.6875 rem ; font-weight : 600 ; text-transform : uppercase ; letter-spacing : 0.05 em ; color : rgba ( 255 , 255 , 255 , 0.45 ); margin-bottom : 0.25 rem ; }
2026-03-16 16:53:25 +00:00
. field-row { display : flex ; align-items : center ; background : rgba ( 255 , 255 , 255 , 0.06 ); border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ); border-radius : 0.5 rem ; overflow : hidden ; min-width : 0 ; }
. field-value { flex : 1 ; min-width : 0 ; padding : 0.625 rem 0.875 rem ; font-family : monospace ; font-size : 0.8125 rem ; color : rgba ( 255 , 255 , 255 , 0.9 ); white-space : nowrap ; overflow : hidden ; text-overflow : ellipsis ; }
2026-03-16 15:46:11 +00:00
. copy-btn { padding : 0.625 rem 0.75 rem ; background : none ; border : none ; border-left : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ); cursor : pointer ; color : rgba ( 255 , 255 , 255 , 0.4 ); transition : all 0.2 s ease ; display : flex ; align-items : center ; }
. copy-btn : hover { color : rgba ( 255 , 255 , 255 , 0.8 ); background : rgba ( 255 , 255 , 255 , 0.05 ); }
. copy-btn . copied { color : #4ade80 ; }
. field-row-split { display : flex ; gap : 0.75 rem ; }
. field-row-split > div { flex : 1 ; }
. help-text { margin-top : 1 rem ; padding-top : 1 rem ; border-top : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ); font-size : 0.875 rem ; color : rgba ( 255 , 255 , 255 , 0.5 ); line-height : 1.5 ; }
2026-01-27 23:57:29 +00:00
2026-03-16 15:46:11 +00:00
/* Modal */
. modal-overlay { display : none ; position : fixed ; inset : 0 ; background : rgba ( 0 , 0 , 0 , 0.8 ); backdrop-filter : blur ( 4 px ); z-index : 50 ; align-items : center ; justify-content : center ; padding : 1 rem ; }
. modal-overlay . visible { display : flex ; }
. modal-content { max-width : 42 rem ; width : 100 % ; max-height : 85 vh ; overflow : hidden ; display : flex ; flex-direction : column ; }
. modal-body { overflow-y : auto ; flex : 1 ; min-height : 0 ; }
. modal-tab { flex : 1 ; padding : 0.5 rem 1 rem ; text-align : center ; border-radius : 0.5 rem ; font-size : 0.875 rem ; font-weight : 500 ; cursor : pointer ; transition : all 0.2 s ease ; color : rgba ( 255 , 255 , 255 , 0.6 ); background : none ; border : none ; }
2026-02-14 16:44:20 +00:00
. modal-tab . active { background : rgba ( 255 , 255 , 255 , 0.2 ); color : white ; }
. modal-tab : not ( . active ) : hover { color : rgba ( 255 , 255 , 255 , 0.9 ); }
. tab-panel { display : none ; }
. tab-panel . active { display : block ; }
2026-03-16 15:46:11 +00:00
. logs-box { background : rgba ( 0 , 0 , 0 , 0.4 ); border-radius : 0.5 rem ; padding : 1 rem ; font-family : monospace ; font-size : 0.75 rem ; color : rgba ( 255 , 255 , 255 , 0.8 ); white-space : pre-wrap ; word-break : break-all ; min-height : 200 px ; }
/* Responsive grid */
@ media ( min-width : 768px ) {
. md-grid-cols-2 { grid-template-columns : repeat ( 2 , 1 fr ); }
. md-grid-cols-3 { grid-template-columns : repeat ( 3 , 1 fr ); }
. md-grid-cols-4 { grid-template-columns : repeat ( 4 , 1 fr ); }
. md-flex-row { flex-direction : row ; }
}
2026-01-27 23:57:29 +00:00
</ style >
</ head >
< body >
2026-03-16 15:46:11 +00:00
< div class = "bg-layer" ></ div >
2026-01-28 00:12:11 +00:00
< div class = "overlay" ></ div >
2026-01-27 23:57:29 +00:00
< div class = "container" >
2026-03-16 15:46:11 +00:00
<!-- Header -->
2026-01-28 00:12:11 +00:00
< div class = "glass-card p-6 mb-6" >
2026-03-16 15:46:11 +00:00
< div class = "flex flex-col md-flex-row items-center gap-4" >
< div class = "logo-border flex-shrink-0" >
< img src = "assets/img/app-icons/lnd.svg" alt = "LND" />
2026-01-27 23:57:29 +00:00
</ div >
2026-03-16 15:46:11 +00:00
< div class = "flex-1 min-w-0" >
< div class = "flex items-center gap-3" >
< h1 class = "text-2xl font-bold text-white" > LND</ h1 >
</ div >
< p class = "text-white-70" > Lightning Network Daemon for instant Bitcoin payments</ p >
< p class = "text-sm text-white-60 mt-2" id = "headerNetwork" > — </ p >
</ div >
< button onclick = "openSettings()" class = "glass-button flex items-center gap-2 px-4 py-2-5 rounded-lg text-sm font-medium flex-shrink-0" >
< svg style = "width:1.25rem;height:1.25rem" fill = "none" stroke = "currentColor" stroke-width = "2" viewBox = "0 0 24 24" >
2026-03-16 15:34:04 +00:00
< path stroke-linecap = "round" stroke-linejoin = "round" d = "M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
< path stroke-linecap = "round" stroke-linejoin = "round" d = "M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
2026-02-14 16:44:20 +00:00
</ svg >
Settings
</ button >
2026-01-28 00:12:11 +00:00
</ div >
</ div >
2026-02-14 16:44:20 +00:00
<!-- Summary strip -->
2026-01-28 00:12:11 +00:00
< div class = "glass-card p-6 mb-6" >
2026-03-16 15:46:11 +00:00
< div class = "grid grid-cols-2 md-grid-cols-4 gap-3" >
< div class = "info-card flex items-center gap-3" >
< div class = "relative" style = "width:0.75rem;height:0.75rem" >
< div class = "status-dot bg-green" id = "statusDot" ></ div >
< div class = "status-dot bg-green animate-ping" id = "statusPing" style = "display:none;opacity:0.75" ></ div >
</ div >
< div >
< p class = "text-xs text-white-60 mb-1" > Node Status</ p >
< p class = "text-sm font-medium text-white" id = "summaryNodeStatus" > — </ p >
</ div >
</ div >
< div class = "info-card flex items-center gap-3" >
< span style = "font-size:1.5rem;color:#fb923c;font-weight:700" > ⚡ </ span >
< div >
< p class = "text-xs text-white-60 mb-1" > Channels</ p >
< p class = "text-sm font-medium text-orange" id = "channelCount" > 0</ p >
2026-01-28 00:12:11 +00:00
</ div >
</ div >
2026-02-03 21:49:30 +00:00
< div class = "info-card flex items-center justify-between" >
2026-01-28 00:12:11 +00:00
< div class = "flex items-center gap-3" >
2026-03-16 15:46:11 +00:00
< div class = "status-dot-sm bg-green" id = "restDot" ></ div >
2026-01-28 00:12:11 +00:00
< div >
2026-03-16 15:46:11 +00:00
< p class = "text-xs text-white-60 mb-1" > REST API</ p >
< p class = "text-sm font-medium text-white" id = "summaryRestStatus" > — </ p >
2026-01-28 00:12:11 +00:00
</ div >
</ div >
2026-03-16 15:46:11 +00:00
< button onclick = "openSettings(); setSettingsTab('rest');" class = "glass-button px-3 py-1-5 rounded-lg text-xs font-medium" > Settings</ button >
2026-01-28 00:12:11 +00:00
</ div >
2026-02-03 21:49:30 +00:00
< div class = "info-card flex items-center justify-between" >
2026-01-28 00:12:11 +00:00
< div class = "flex items-center gap-3" >
2026-03-16 15:46:11 +00:00
< div class = "status-dot-sm bg-green" id = "grpcDot" ></ div >
2026-01-28 00:12:11 +00:00
< div >
2026-03-16 15:46:11 +00:00
< p class = "text-xs text-white-60 mb-1" > gRPC</ p >
< p class = "text-sm font-medium text-white" id = "summaryGrpcStatus" > — </ p >
2026-01-28 00:12:11 +00:00
</ div >
</ div >
2026-03-16 15:46:11 +00:00
< button onclick = "openSettings(); setSettingsTab('logs');" class = "glass-button px-3 py-1-5 rounded-lg text-xs font-medium" > Logs</ button >
2026-01-28 00:12:11 +00:00
</ div >
2026-01-27 23:57:29 +00:00
</ div >
</ div >
2026-03-16 15:46:11 +00:00
<!-- Wallet -->
2026-02-14 16:44:20 +00:00
< div class = "glass-card p-6 mb-8" >
2026-03-16 15:46:11 +00:00
< div class = "flex items-start gap-4 mb-4" >
< div class = "icon-box-sm flex-shrink-0" >
< svg style = "width:1.5rem;height:1.5rem;color:#fb923c" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</ svg >
2026-01-27 23:57:29 +00:00
</ div >
2026-03-16 15:46:11 +00:00
< div class = "flex-1" >
< h2 class = "text-xl font-semibold text-white mb-1" > Wallet</ h2 >
< p class = "text-white-60 text-sm" > Balance, Receive, and Send will appear here when connected to your node.</ p >
2026-02-14 16:44:20 +00:00
</ div >
2026-03-16 15:46:11 +00:00
</ div >
< div class = "grid grid-cols-1 md-grid-cols-3 gap-3 mb-4" >
< div class = "balance-card" >
< p class = "text-xs text-white-50 uppercase tracking-wide mb-1" > Spendable</ p >
< p class = "text-lg font-semibold text-white" id = "balanceSpendable" > — </ p >
</ div >
< div class = "balance-card" >
< p class = "text-xs text-white-50 uppercase tracking-wide mb-1" > Lightning</ p >
< p class = "text-lg font-semibold text-white" id = "balanceLightning" > — </ p >
</ div >
< div class = "balance-card" >
< p class = "text-xs text-white-50 uppercase tracking-wide mb-1" > Total</ p >
< p class = "text-lg font-semibold text-white" id = "balanceTotal" > — </ p >
2026-01-27 23:57:29 +00:00
</ div >
</ div >
2026-02-14 16:44:20 +00:00
< div class = "flex gap-4" >
2026-03-16 15:46:11 +00:00
< button class = "glass-button px-6 py-3 rounded-lg font-medium" disabled > Receive</ button >
< button class = "glass-button px-6 py-3 rounded-lg font-medium" disabled > Send</ button >
2026-01-28 00:12:11 +00:00
</ div >
2026-03-16 15:46:11 +00:00
< p class = "text-white-50 text-xs mt-4" > Recent activity will be listed here.</ p >
2026-01-28 00:12:11 +00:00
</ div >
2026-03-16 15:34:04 +00:00
<!-- Connect Your Wallet -->
< div class = "glass-card p-6 mb-8" >
< h2 class = "text-xl font-semibold text-white mb-1" > Connect Your Wallet</ h2 >
2026-03-16 15:46:11 +00:00
< p class = "text-white-70 text-sm mb-4" id = "connSubtitle" > Use a wallet like Zeus, Zap, or BlueWallet to connect remotely.</ p >
2026-03-16 15:34:04 +00:00
<!-- Mode selector -->
< div class = "mb-4" >
2026-03-16 15:46:11 +00:00
< select id = "connMode" onchange = "updateConnInfo()" class = "conn-select" >
2026-03-16 15:34:04 +00:00
< option value = "rest-tor" > REST (Tor)</ option >
< option value = "rest-local" > REST (Local Network)</ option >
< option value = "grpc-tor" > gRPC (Tor)</ option >
< option value = "grpc-local" > gRPC (Local Network)</ option >
</ select >
</ div >
<!-- Connection display -->
2026-03-16 15:46:11 +00:00
< div class = "conn-layout" id = "connDisplay" >
< div class = "qr-box" id = "lndQrBox" >
< div style = "color:#999;font-size:12px;text-align:center;padding:2rem" > Loading...</ div >
2026-03-16 15:34:04 +00:00
</ div >
2026-03-16 15:46:11 +00:00
< div class = "conn-fields" >
2026-03-16 15:34:04 +00:00
< div >
2026-03-16 15:46:11 +00:00
< div class = "field-label" > Host</ div >
< div class = "field-row" >
< span class = "field-value" id = "connHost" > — </ span >
< button class = "copy-btn" onclick = "copyEl('connHost', this)" title = "Copy" >
< svg width = "16" height = "16" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< rect x = "9" y = "9" width = "13" height = "13" rx = "2" ry = "2" stroke-width = "2" />< path d = "M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width = "2" /></ svg >
2026-03-16 15:34:04 +00:00
</ button >
</ div >
</ div >
2026-03-16 15:46:11 +00:00
< div class = "field-row-split" >
< div >
< div class = "field-label" > Port</ div >
< div class = "field-row" >
< span class = "field-value" id = "connPort" > — </ span >
< button class = "copy-btn" onclick = "copyEl('connPort', this)" title = "Copy" >
< svg width = "16" height = "16" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< rect x = "9" y = "9" width = "13" height = "13" rx = "2" ry = "2" stroke-width = "2" />< path d = "M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width = "2" /></ svg >
2026-03-16 15:34:04 +00:00
</ button >
</ div >
</ div >
2026-03-16 15:46:11 +00:00
< div >
< div class = "field-label" > Protocol</ div >
< div class = "field-row" >
< span class = "field-value" id = "connProto" > — </ span >
2026-03-16 15:34:04 +00:00
</ div >
</ div >
</ div >
2026-03-16 15:46:11 +00:00
< button onclick = "copyLndconnectUri()" class = "glass-button w-full mt-2 px-4 py-2-5 rounded-lg text-sm font-medium" id = "copyUriBtn" > Copy lndconnect URI</ button >
2026-03-16 15:34:04 +00:00
</ div >
</ div >
2026-03-16 15:46:11 +00:00
< div class = "help-text" >
Scan the QR code with < strong style = "color:rgba(255,255,255,0.8)" > Zeus</ strong > , < strong style = "color:rgba(255,255,255,0.8)" > Zap</ strong > , or < strong style = "color:rgba(255,255,255,0.8)" > BlueWallet</ strong > to connect. Tor mode recommended for remote access.
2026-03-16 15:34:04 +00:00
</ div >
</ div >
2026-01-28 00:12:11 +00:00
</ div >
2026-02-14 16:44:20 +00:00
<!-- Tabbed Settings Modal -->
2026-03-16 15:46:11 +00:00
< div class = "modal-overlay" id = "settingsModal" >
< div class = "glass-card p-6 modal-content" >
2026-02-14 16:44:20 +00:00
< div class = "flex justify-between items-center mb-4 flex-shrink-0" >
< h2 class = "text-2xl font-bold text-white" > Settings</ h2 >
2026-03-16 15:46:11 +00:00
< button onclick = "closeSettings()" class = "glass-button px-3 py-2 rounded-lg text-xl font-medium" > × </ button >
2026-01-28 00:12:11 +00:00
</ div >
2026-03-16 15:46:11 +00:00
< div class = "flex gap-2 mb-4 flex-shrink-0" style = "background:rgba(255,255,255,0.06);border-radius:0.5rem;padding:0.25rem" >
< button class = "modal-tab active" data-tab = "node" > Node Status</ button >
< button class = "modal-tab" data-tab = "rest" > REST API</ button >
< button class = "modal-tab" data-tab = "grpc" > gRPC</ button >
< button class = "modal-tab" data-tab = "logs" > Logs</ button >
2026-01-27 23:57:29 +00:00
</ div >
2026-03-16 15:46:11 +00:00
< div class = "modal-body" style = "display:flex;flex-direction:column;gap:1rem" >
2026-02-14 16:44:20 +00:00
<!-- Node Status tab -->
< div id = "panel-node" class = "tab-panel active" >
< div class = "flex items-start gap-4 mb-4" >
2026-03-16 15:46:11 +00:00
< div class = "icon-box-sm flex-shrink-0" >
< svg style = "width:1.5rem;height:1.5rem;color:rgba(255,255,255,0.8)" 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 >
2026-02-14 16:44:20 +00:00
</ div >
< div class = "flex-1" >
< h3 class = "text-lg font-semibold text-white mb-1" > Node Status</ h3 >
2026-03-16 15:46:11 +00:00
< p class = "text-white-70 text-sm" > Lightning node information</ p >
2026-02-14 16:44:20 +00:00
</ div >
</ div >
2026-03-16 15:46:11 +00:00
< div style = "display:flex;flex-direction:column;gap:0.75rem" >
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > Node Status</ span >
< span class = "text-green text-sm font-medium" id = "modalNodeStatus" > — </ span >
2026-02-14 16:44:20 +00:00
</ div >
2026-03-16 15:46:11 +00:00
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > Network</ span >
< span class = "text-white-60 text-sm" id = "modalNetwork" > — </ span >
2026-02-14 16:44:20 +00:00
</ div >
2026-03-16 15:46:11 +00:00
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > Version</ span >
< span class = "text-white-60 text-sm" id = "modalVersion" > — </ span >
2026-02-14 16:44:20 +00:00
</ div >
</ div >
2026-03-16 15:46:11 +00:00
< div style = "display:flex;flex-direction:column;gap:0.75rem;margin-top:1rem" >
< div class = "stat-row" style = "flex-direction:column;align-items:flex-start" >
2026-02-14 16:44:20 +00:00
< div class = "font-semibold text-white mb-1" > Network Mode</ div >
2026-03-16 15:46:11 +00:00
< div class = "text-white-70 text-sm" id = "modalNetworkMode" > — </ div >
2026-02-14 16:44:20 +00:00
</ div >
2026-03-16 15:46:11 +00:00
< div class = "stat-row" style = "flex-direction:column;align-items:flex-start" >
2026-02-14 16:44:20 +00:00
< div class = "font-semibold text-white mb-1" > Bitcoin Backend</ div >
2026-03-16 15:46:11 +00:00
< div class = "text-white-70 text-sm" id = "modalBitcoinBackend" > — </ div >
2026-02-14 16:44:20 +00:00
</ div >
</ div >
</ div >
<!-- REST API tab -->
< div id = "panel-rest" class = "tab-panel" >
< div class = "flex items-start gap-4 mb-4" >
2026-03-16 15:46:11 +00:00
< div class = "icon-box-sm flex-shrink-0" >
< svg style = "width:1.5rem;height:1.5rem;color:rgba(255,255,255,0.8)" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01" /></ svg >
2026-02-14 16:44:20 +00:00
</ div >
< div class = "flex-1" >
< h3 class = "text-lg font-semibold text-white mb-1" > REST API</ h3 >
2026-03-16 15:46:11 +00:00
< p class = "text-white-70 text-sm" > HTTP REST API access</ p >
2026-02-14 16:44:20 +00:00
</ div >
</ div >
2026-03-16 15:46:11 +00:00
< div style = "display:flex;flex-direction:column;gap:0.75rem" >
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > REST Endpoint</ span >
< span class = "text-white-60 text-sm font-mono" id = "modalRestEndpoint" > — </ span >
2026-02-14 16:44:20 +00:00
</ div >
2026-03-16 15:46:11 +00:00
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > API Status</ span >
< span class = "text-green text-sm font-medium" id = "modalRestStatus" > — </ span >
2026-02-14 16:44:20 +00:00
</ div >
2026-03-16 15:46:11 +00:00
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > API Version</ span >
< span class = "text-white-60 text-sm" id = "modalRestVersion" > v1</ span >
2026-02-14 16:44:20 +00:00
</ div >
</ div >
2026-03-16 15:46:11 +00:00
< button class = "info-card-button w-full mt-4 text-sm font-medium py-3 rounded-lg" onclick = "copyRESTInfo()" style = "text-align:center;display:block" > Copy REST Info</ button >
2026-02-14 16:44:20 +00:00
</ div >
<!-- gRPC tab -->
< div id = "panel-grpc" class = "tab-panel" >
< div class = "flex items-start gap-4 mb-4" >
2026-03-16 15:46:11 +00:00
< div class = "icon-box-sm flex-shrink-0" >
< svg style = "width:1.5rem;height:1.5rem;color:rgba(255,255,255,0.8)" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01" /></ svg >
2026-02-14 16:44:20 +00:00
</ div >
< div class = "flex-1" >
< h3 class = "text-lg font-semibold text-white mb-1" > gRPC Connection</ h3 >
2026-03-16 15:46:11 +00:00
< p class = "text-white-70 text-sm" > High-performance gRPC API</ p >
2026-02-14 16:44:20 +00:00
</ div >
</ div >
2026-03-16 15:46:11 +00:00
< div style = "display:flex;flex-direction:column;gap:0.75rem" >
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > gRPC Host</ span >
< span class = "text-white-60 text-sm font-mono" id = "modalGrpcHost" > — </ span >
2026-02-14 16:44:20 +00:00
</ div >
2026-03-16 15:46:11 +00:00
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > gRPC Status</ span >
< span class = "text-green text-sm font-medium" id = "modalGrpcStatus" > — </ span >
2026-02-14 16:44:20 +00:00
</ div >
2026-03-16 15:46:11 +00:00
< div class = "stat-row" >
< span class = "text-white-80 text-sm" > P2P Port</ span >
< span class = "text-white-60 text-sm font-mono" > 9735</ span >
2026-02-14 16:44:20 +00:00
</ div >
</ div >
</ div >
<!-- Logs tab -->
< div id = "panel-logs" class = "tab-panel" >
< div class = "flex justify-between items-center mb-3" >
< h3 class = "text-lg font-semibold text-white" > Node Logs</ h3 >
2026-03-16 15:46:11 +00:00
< button onclick = "loadLogs()" class = "glass-button px-3 py-1-5 rounded-lg text-xs font-medium" > Refresh</ button >
2026-02-14 16:44:20 +00:00
</ div >
2026-03-16 15:46:11 +00:00
< div class = "logs-box" id = "logsContent" > Loading logs...</ div >
2026-02-14 16:44:20 +00:00
</ div >
2026-01-27 23:57:29 +00:00
</ div >
</ div >
</ div >
2026-03-16 15:34:04 +00:00
< script src = "qrcode.js" ></ script >
2026-01-27 23:57:29 +00:00
< script >
2026-02-14 16:44:20 +00:00
const REST_PORT = 8080 ;
const GRPC_PORT = 10009 ;
const P2P_PORT = 9735 ;
const host = window . location . hostname ;
function getBackendUrl () {
const params = new URLSearchParams ( window . location . search );
2026-03-16 16:06:07 +00:00
return params . get ( 'backend' ) || ( window . location . protocol + '//' + window . location . hostname );
2026-02-14 16:44:20 +00:00
}
function setSettingsTab ( tabId ) {
document . querySelectorAll ( '.modal-tab' ). forEach ( btn => {
btn . classList . toggle ( 'active' , btn . getAttribute ( 'data-tab' ) === tabId );
2026-01-27 23:57:29 +00:00
});
2026-02-14 16:44:20 +00:00
document . querySelectorAll ( '.tab-panel' ). forEach ( panel => {
panel . classList . toggle ( 'active' , panel . id === 'panel-' + tabId );
});
if ( tabId === 'logs' ) loadLogs ();
}
document . querySelectorAll ( '.modal-tab' ). forEach ( btn => {
btn . addEventListener ( 'click' , () => setSettingsTab ( btn . getAttribute ( 'data-tab' )));
});
function copyRESTInfo () {
const endpoint = host + ':' + REST_PORT ;
const info = 'REST API: http://' + endpoint + '\nAPI Version: v1' ;
navigator . clipboard . writeText ( info ). then (() => alert ( 'REST info copied to clipboard!' ));
2026-01-27 23:57:29 +00:00
}
function openSettings () {
2026-03-16 15:46:11 +00:00
document . getElementById ( 'settingsModal' ). classList . add ( 'visible' );
2026-01-27 23:57:29 +00:00
}
function closeSettings () {
2026-03-16 15:46:11 +00:00
document . getElementById ( 'settingsModal' ). classList . remove ( 'visible' );
2026-01-27 23:57:29 +00:00
}
2026-02-14 16:44:20 +00:00
function applyLiveData ( data ) {
if ( data . getinfo ) {
const g = data . getinfo ;
const status = g . synced_to_chain ? 'Running' : 'Waiting for chain…' ;
const network = ( g . chains && g . chains [ 0 ]) ? g . chains [ 0 ]. network || '—' : '—' ;
const version = g . version || '—' ;
setText ( 'headerNetwork' , 'Network: ' + network );
setText ( 'summaryNodeStatus' , status );
setText ( 'modalNodeStatus' , status );
setText ( 'modalNetwork' , network );
setText ( 'modalVersion' , version );
setText ( 'modalNetworkMode' , network );
setText ( 'modalBitcoinBackend' , '—' );
document . getElementById ( 'statusPing' ). style . display = g . synced_to_chain ? 'block' : 'none' ;
}
if ( data . channelCount !== undefined ) {
setText ( 'channelCount' , String ( data . channelCount ));
}
setText ( 'modalRestEndpoint' , host + ':' + REST_PORT );
setText ( 'modalRestStatus' , data . restReachable ? 'Active' : '—' );
setText ( 'summaryRestStatus' , data . restReachable ? 'Active' : '—' );
setText ( 'modalGrpcHost' , host + ':' + GRPC_PORT );
setText ( 'modalGrpcStatus' , data . grpcReachable ? 'Connected' : '—' );
setText ( 'summaryGrpcStatus' , data . grpcReachable ? 'Connected' : '—' );
2026-01-27 23:57:29 +00:00
}
2026-02-14 16:44:20 +00:00
function setText ( id , text ) {
const el = document . getElementById ( id );
if ( el ) el . textContent = text ;
2026-01-27 23:57:29 +00:00
}
2026-02-14 16:44:20 +00:00
async function loadLogs () {
2026-01-27 23:57:29 +00:00
const logsContent = document . getElementById ( 'logsContent' );
2026-02-14 16:44:20 +00:00
const backendUrl = getBackendUrl ();
if ( backendUrl ) {
logsContent . textContent = 'Loading logs...' ;
try {
const res = await fetch ( backendUrl + '/api/container/logs?app_id=lnd&lines=200' );
if ( ! res . ok ) throw new Error ( res . statusText );
const json = await res . json ();
const lines = json . result || json . logs || ( Array . isArray ( json ) ? json : []);
logsContent . textContent = Array . isArray ( lines ) ? lines . join ( '\n' ) : String ( lines );
} catch ( e ) {
logsContent . textContent = 'Could not load logs: ' + e . message ;
}
} else {
logsContent . textContent = 'Open this app with ?backend=http://HOST:5678 to load logs from the server.' ;
}
2026-01-27 23:57:29 +00:00
}
2026-02-14 16:44:20 +00:00
async function fetchLiveData () {
const backendUrl = getBackendUrl ();
const data = { channelCount : 0 , restReachable : false , grpcReachable : false };
if ( backendUrl ) {
try {
const getinfoRes = await fetch ( backendUrl + '/proxy/lnd/v1/getinfo' );
if ( getinfoRes . ok ) {
data . getinfo = await getinfoRes . json ();
data . restReachable = true ;
}
} catch ( _ ) {}
try {
const chRes = await fetch ( backendUrl + '/proxy/lnd/v1/channels' );
if ( chRes . ok ) {
const ch = await chRes . json ();
data . channelCount = ( ch . channels && ch . channels . length ) || 0 ;
}
} catch ( _ ) {}
data . grpcReachable = data . restReachable ;
2026-01-27 23:57:29 +00:00
}
2026-02-14 16:44:20 +00:00
applyLiveData ( data );
}
2026-01-27 23:57:29 +00:00
2026-02-14 16:44:20 +00:00
document . addEventListener ( 'DOMContentLoaded' , () => {
fetchLiveData ();
2026-01-27 23:57:29 +00:00
});
2026-02-14 16:44:20 +00:00
document . addEventListener ( 'keydown' , ( e ) => {
if ( e . key === 'Escape' ) closeSettings ();
});
document . getElementById ( 'settingsModal' ). addEventListener ( 'click' , ( e ) => {
if ( e . target . id === 'settingsModal' ) closeSettings ();
2026-01-27 23:57:29 +00:00
});
2026-03-16 15:34:04 +00:00
// --- Connect Your Wallet ---
let lndConnInfo = null ;
function renderQR ( containerId , text ) {
const container = document . getElementById ( containerId );
if ( ! container ) return ;
container . innerHTML = '' ;
try {
const qr = qrcode ( 0 , 'L' );
qr . addData ( text );
qr . make ();
container . innerHTML = qr . createImgTag ( 3 , 0 );
} catch ( e ) {
container . innerHTML = '<div style="color:#999;font-size:11px;text-align:center;padding:2rem">QR too large for this mode</div>' ;
}
}
function buildLndconnectUri ( connHost , connPort , cert , macaroon , isTor ) {
let uri = 'lndconnect://' + connHost + ':' + connPort + '?' ;
if ( ! isTor && cert ) uri += 'cert=' + cert + '&' ;
uri += 'macaroon=' + macaroon ;
return uri ;
}
function updateConnInfo () {
if ( ! lndConnInfo ) return ;
const mode = document . getElementById ( 'connMode' ). value ;
const isTor = mode . includes ( 'tor' );
const isRest = mode . includes ( 'rest' );
const port = isRest ? lndConnInfo . rest_port : lndConnInfo . grpc_port ;
const connHost = isTor && lndConnInfo . tor_onion ? lndConnInfo . tor_onion : host ;
const proto = isRest ? 'REST' : 'gRPC' ;
setText ( 'connHost' , connHost );
setText ( 'connPort' , String ( port ));
setText ( 'connProto' , proto );
if ( isTor && ! lndConnInfo . tor_onion ) {
document . getElementById ( 'lndQrBox' ). innerHTML = '<div style="color:#999;font-size:12px;text-align:center;padding:2rem">Tor not configured for LND</div>' ;
document . getElementById ( 'connSubtitle' ). textContent = 'Tor hidden service not available. Use Local Network mode.' ;
return ;
}
document . getElementById ( 'connSubtitle' ). textContent = 'Scan QR with Zeus, Zap, or BlueWallet to connect.' ;
const uri = buildLndconnectUri ( connHost , port , lndConnInfo . cert_base64url , lndConnInfo . macaroon_base64url , isTor );
renderQR ( 'lndQrBox' , uri );
}
function copyEl ( id , btn ) {
const text = document . getElementById ( id ). textContent . trim ();
if ( ! text || text === '—' ) return ;
navigator . clipboard . writeText ( text ). then (() => {
const orig = btn . innerHTML ;
btn . innerHTML = '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>' ;
btn . style . color = '#4ade80' ;
setTimeout (() => { btn . innerHTML = orig ; btn . style . color = '' ; }, 1500 );
});
}
function copyLndconnectUri () {
if ( ! lndConnInfo ) return ;
const mode = document . getElementById ( 'connMode' ). value ;
const isTor = mode . includes ( 'tor' );
const isRest = mode . includes ( 'rest' );
const port = isRest ? lndConnInfo . rest_port : lndConnInfo . grpc_port ;
const connHost = isTor && lndConnInfo . tor_onion ? lndConnInfo . tor_onion : host ;
const uri = buildLndconnectUri ( connHost , port , lndConnInfo . cert_base64url , lndConnInfo . macaroon_base64url , isTor );
const btn = document . getElementById ( 'copyUriBtn' );
navigator . clipboard . writeText ( uri ). then (() => {
const orig = btn . textContent ;
btn . textContent = 'Copied!' ;
btn . style . color = '#4ade80' ;
setTimeout (() => { btn . textContent = orig ; btn . style . color = '' ; }, 1500 );
});
}
async function fetchConnectInfo () {
try {
2026-03-16 16:06:07 +00:00
const resp = await fetch ( window . location . protocol + '//' + window . location . hostname + '/lnd-connect-info' );
2026-03-16 15:34:04 +00:00
if ( ! resp . ok ) throw new Error ( 'HTTP ' + resp . status );
const data = await resp . json ();
if ( data . cert_base64url ) {
lndConnInfo = data ;
2026-03-16 16:29:26 +00:00
// Auto-select local mode when Tor is not available
if ( ! data . tor_onion ) {
document . getElementById ( 'connMode' ). value = 'rest-local' ;
document . getElementById ( 'connSubtitle' ). textContent = 'Tor hidden service not available. Use Local Network mode.' ;
}
2026-03-16 15:34:04 +00:00
try { updateConnInfo (); } catch ( ue ) {
document . getElementById ( 'lndQrBox' ). innerHTML = '<div style="color:#f87171;font-size:11px;text-align:center;padding:1rem">Update error: ' + ue . message + '</div>' ;
return ;
}
} else if ( data . error ) {
throw new Error ( data . error );
}
} catch ( e ) {
document . getElementById ( 'lndQrBox' ). innerHTML = '<div style="color:#f87171;font-size:12px;text-align:center;padding:2rem">' + e . message + '</div>' ;
}
}
fetchConnectInfo ();
2026-01-27 23:57:29 +00:00
</ script >
</ body >
</ html >