PageSourceSearch

https://bcmonster.com/vrsc/static/daemon-health.js?v=3

js bcmonster.com collected 2026-10-04 14:21:31 UTC 11,382 bytes, 201 lines download raw bytes

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 { '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[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.