1// daemon-health.js â BCMonster, both pools 2// 3// Header indicator for /api/daemon_health (libs/daemonHealthMonitor.js). 4// 5// That endpoint exists to catch a daemon that is UP and answering RPC but has 6// fallen off the network â the 2026-08-21 bitcoinii case, where a site internet 7// outage froze the daemon's chain view at height 57700 while the real network 8// mined on. getblocktemplate kept answering perfectly, so nothing noticed until 9// a share solved the dead template and was submitted six hours late for a height 10// already taken. It was orphaned on arrival. 11// 12// Standalone on purpose. This site has TWO header systems â static/bcm-header.js 13// renders the header on most pages, but index.html (and the Verus rig's copy) 14// carries its own hand-written header markup and only mentions bcm-header.js in a 15// comment. Putting the widget in bcm-header.js therefore left it missing from the 16// busiest page on the site. This file owns its own CSS, finds whichever header is 17// present, and builds the element itself, so there is one implementation 18// regardless of which header the page uses. 19(function () { 20 if (window.__bcmDaemonHealth) return; // never double-mount 21 window.__bcmDaemonHealth = true; 22 23 var COLORS = { ok:'#22c55e', warning:'#f59e0b', syncing:'#f59e0b', 24 behind:'#ef4444', isolated:'#ef4444', unreachable:'#ef4444' }; 25 var WORDS = { ok:'All daemons healthy', warning:'Connectivity marginal', 26 syncing:'Syncing', behind:'Behind the network', 27 isolated:'Daemon isolated', unreachable:'Daemon unreachable' }; 28 29 // A page under the /vrsc proxy must ask THAT pool, not the main one. 30 function root() { 31 try { return /^\/(vrsc|verus)(\/|$)/i.test(window.location.pathname) ? '/vrsc' : ''; } 32 catch (e) { return ''; } 33 } 34 function esc(t) { 35 return String(t == null ? '' : t).replace(/[&<>"']/g, function (c) { 36 return { '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]; 37 }); 38 } 39 40 function injectCss() { 41 if (document.getElementById('bcm-health-css')) return; 42 var st = document.createElement('style'); 43 st.id = 'bcm-health-css'; 44 st.textContent = [ 45 /* Tiny and silent when all is well â a green dot people stop noticing is 46 the point. It only grows a text label when something is actually wrong, 47 so the first time it draws the eye is the first time it matters. */ 48 '.bcm-health{position:relative;display:none;align-items:center;flex-shrink:0;}', 49 '.bcm-health.on{display:inline-flex;}', 50 '.bcm-health-btn{display:inline-flex;align-items:center;gap:.4rem;appearance:none;-webkit-appearance:none;', 51 'padding:.4rem .6rem;border-radius:8px;cursor:pointer;font-weight:600;font-size:.78rem;line-height:1;', 52 'background:rgba(255,255,255,.12);color:inherit;border:1px solid rgba(255,255,255,.22);', 53 'transition:background .2s,border-color .2s;white-space:nowrap;}', 54 '.bcm-health-btn:hover{background:rgba(255,255,255,.22);}', 55 '[data-theme="light"] .bcm-health-btn{background:rgba(0,0,0,.06);border-color:rgba(0,0,0,.14);}', 56 '[data-theme="light"] .bcm-health-btn:hover{background:rgba(0,0,0,.12);}', 57 '.bcm-health-dot{width:9px;height:9px;border-radius:50%;background:#22c55e;flex-shrink:0;', 58 'box-shadow:0 0 0 2px rgba(34,197,94,.20);}', 59 '.bcm-health[data-level="warning"] .bcm-health-dot,.bcm-health[data-level="syncing"] .bcm-health-dot', 60 '{background:#f59e0b;box-shadow:0 0 0 2px rgba(245,158,11,.22);}', 61 '.bcm-health[data-level="behind"] .bcm-health-dot,.bcm-health[data-level="isolated"] .bcm-health-dot,', 62 '.bcm-health[data-level="unreachable"] .bcm-health-dot', 63 '{background:#ef4444;box-shadow:0 0 0 2px rgba(239,68,68,.24);animation:bcmHealthPulse 1.6s ease-in-out infinite;}', 64 '@keyframes bcmHealthPulse{0%,100%{opacity:1;}50%{opacity:.35;}}',
65 '@media(prefers-reduced-motion:reduce){.bcm-health-dot{animation:none!important;}}', 66 '.bcm-health-label{display:none;}', 67 '.bcm-health:not([data-level="ok"]) .bcm-health-label{display:inline;}', 68 '.bcm-health-menu{display:none;position:absolute;top:calc(100% + 6px);right:0;width:300px;max-height:70vh;', 69 'overflow:auto;background:#fff;border:1px solid rgba(65,105,225,.25);border-radius:10px;', 70 'box-shadow:0 8px 24px rgba(0,0,0,.16);z-index:3000;padding:.45rem;}', 71 '[data-theme="dark"] .bcm-health-menu{background:#0f1e38;border-color:rgba(255,255,255,.12);box-shadow:0 8px 24px rgba(0,0,0,.5);}', 72 '.bcm-health-menu.open{display:block;}', 73 // Never wider than the viewport. The JS clamp below handles WHERE it 74 // sits; this handles how wide it may get on a 360px phone. 75 '@media(max-width:640px){.bcm-health-menu{width:auto;min-width:220px;max-width:calc(100vw - 16px);}}', 76 // On a phone the header's action row is already at its width budget â 77 // measured: adding this control pushed a 390px-wide header from 179px 78 // to 218px, a whole extra wrapped row. Strip the button chrome down to 79 // the dot itself there (the label is already hidden while healthy), so 80 // it costs ~14px instead of ~40px and no longer forces the wrap. 81 '@media(max-width:640px){', 82 '.bcm-health-btn{padding:0;border:none;background:none;}', 83 '.bcm-health-btn:hover{background:none;}', 84 '[data-theme="light"] .bcm-health-btn{background:none;border:none;}', 85 '.bcm-health-dot{width:10px;height:10px;}', 86 '}', 87 '.bcm-health-row{display:flex;align-items:center;gap:.5rem;padding:.34rem .4rem;border-radius:6px;', 88 'font-size:.76rem;color:#1e293b;}', 89 '[data-theme="dark"] .bcm-health-row{color:#e2e8f0;}', 90 '.bcm-health-row .nm{font-weight:700;min-width:88px;text-transform:uppercase;letter-spacing:.02em;font-size:.68rem;}', 91 '.bcm-health-row .dt{width:8px;height:8px;border-radius:50%;flex-shrink:0;}', 92 '.bcm-health-row small{opacity:.68;margin-left:auto;text-align:right;}', 93 '.bcm-health-note{font-size:.68rem;opacity:.55;padding:.4rem .4rem .1rem;color:#1e293b;}', 94 '[data-theme="dark"] .bcm-health-note{color:#e2e8f0;}' 95 ].join(''); 96 document.head.appendChild(st); 97 } 98 99 // Try every header shape this site actually uses, most specific first. 100 function findHost() { 101 return document.querySelector('.bcmh-right') // bcm-header.js pages 102 || document.querySelector('header .theme-toggle') // index.html's own header 103 || document.querySelector('.hdr-actions, .hdr-right') 104 || (document.getElementById('notify-btn') || {}).parentNode 105 || null; 106 } 107 108 var el, btn, menu, label; 109 110 function mount() { 111 var host = findHost(); 112 if (!host) return false; 113 injectCss(); 114 el = document.createElement('div'); 115 el.className = 'bcm-health'; 116 el.id = 'bcm-health'; 117 el.setAttribute('data-level', 'ok'); 118 el.setAttribute('translate', 'no'); 119 el.innerHTML = 120 '<button type="button" class="bcm-health-btn" id="bcm-health-btn" aria-label="Daemon health">' + 121 '<span class="bcm-health-dot"></span>' + 122 '<span class="bcm-health-label" id="bcm-health-label"></span>' + 123 '</button>' + 124 '<div class="bcm-health-menu" id="bcm-health-menu"></div>'; 125 host.insertBefore(el, host.firstChild); 126 btn = el.querySelector('#bcm-health-btn'); 127 menu = el.querySelector('#bcm-health-menu'); 128 label = el.querySelector('#bcm-health-label'); 129 // Keep the panel on screen. It is anchored right:0 to its button, so when 130 // that button sits on the LEFT of a narrow header the 300px panel extends 131 // leftward past the viewport edge â measured 61px off at 360px wide.
132 // Re-anchor by adjusting `right` until both edges are inside the viewport. 133 function clampMenu() { 134 menu.style.right = '0px'; 135 var pad = 8; 136 var vw = document.documentElement.clientWidth; 137 var r = menu.getBoundingClientRect(); 138 if (r.left < pad) { 139 menu.style.right = (0 - (pad - r.left)) + 'px'; 140 } else if (r.right > vw - pad) { 141 menu.style.right = (0 + (r.right - (vw - pad))) + 'px'; 142 } 143 } 144 btn.addEventListener('click', function (e) { 145 e.stopPropagation(); 146 var opening = !menu.classList.contains('open'); 147 menu.classList.toggle('open'); 148 if (opening) clampMenu(); 149 }); 150 window.addEventListener('resize', function () { if (menu.classList.contains('open')) clampMenu(); }); 151 menu.addEventListener('click', function (e) { e.stopPropagation(); }); 152 document.addEventListener('click', function () { menu.classList.remove('open'); }); 153 return true; 154 } 155 156 function render(d) { 157 if (!el || !d || !d.coins || !d.checkedCoins) return; 158 var overall = d.overall || 'ok'; 159 el.setAttribute('data-level', overall); 160 el.classList.add('on'); 161 label.textContent = overall === 'ok' ? '' : (WORDS[overall] || overall); 162 btn.setAttribute('title', 'Daemon health â ' + (WORDS[overall] || overall)); 163 menu.innerHTML = Object.keys(d.coins).sort().map(function (c) { 164 var r = d.coins[c] || {}, lv = r.level || 'ok'; 165 var detail = lv === 'ok' 166 ? (r.connections != null ? r.connections + ' peers' : '') 167 : esc(r.reason || lv); 168 return '<div class="bcm-health-row">' + 169 '<span class="dt" style="background:' + (COLORS[lv] || '#94a3b8') + '"></span>' + 170 '<span class="nm">' + esc(c) + '</span>' + 171 '<small>' + detail + '</small>' + 172 '</div>'; 173 }).join('') + 174 '<div class="bcm-health-note">Peer count and height vs. the network, checked every minute.</div>'; 175 } 176 177 // Fails silent by design: if the fetch errors or the endpoint isn't deployed on 178 // this pool, the dot stays hidden. A monitor that cries wolf when the MONITOR is 179 // what broke is worse than no monitor. 180 function poll() { 181 fetch(root() + '/api/daemon_health', { cache: 'no-store' }) 182 .then(function (r) { return r.ok ? r.json() : null; }) 183 .then(render) 184 .catch(function () {}); 185 } 186 187 function boot() { 188 if (!mount()) { 189 // Header may be injected after us by bcm-header.js â retry briefly. 190 var tries = 0; 191 var iv = setInterval(function () { 192 if (mount() || ++tries > 20) { clearInterval(iv); if (el) { poll(); setInterval(poll, 60000); } } 193 }, 250); 194 return; 195 } 196 poll(); 197 setInterval(poll, 60000); 198 } 199 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); 200 else boot(); 201})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.