blockyard 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +929 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +191 -4
- package/SECURITY.md +38 -0
- package/bin/blockyard.js +41 -0
- package/config/pool-map.json +2620 -0
- package/docs/API.md +1577 -0
- package/docs/ARCHITECTURE.md +1394 -0
- package/docs/AUTO-UPDATE.md +269 -0
- package/docs/CONFIGURATION.md +847 -0
- package/docs/DEFECTS.md +813 -0
- package/docs/EFFECTS-AGENTS.md +448 -0
- package/docs/GETTING-STARTED.md +205 -0
- package/docs/INSTALL.md +547 -0
- package/docs/MEASUREMENTS.md +1401 -0
- package/docs/RULES.md +681 -0
- package/docs/SECURITY-AUDIT-2026-09-14.md +177 -0
- package/docs/SECURITY-AUDIT.md +258 -0
- package/docs/SECURITY.md +212 -0
- package/docs/TROUBLESHOOTING.md +332 -0
- package/docs/USER-GUIDE.md +1262 -0
- package/package.json +53 -5
- package/public/404.html +9 -0
- package/public/css/app.css +2009 -0
- package/public/donate-qr.png +0 -0
- package/public/index.html +1085 -0
- package/public/js/about.js +112 -0
- package/public/js/agents.js +1141 -0
- package/public/js/app.js +1386 -0
- package/public/js/arkanoid.js +806 -0
- package/public/js/blockanoid.js +347 -0
- package/public/js/blockout.js +347 -0
- package/public/js/blockpack.js +428 -0
- package/public/js/blockscene3d.js +2830 -0
- package/public/js/breakout.js +224 -0
- package/public/js/charts.js +635 -0
- package/public/js/depthchart.js +315 -0
- package/public/js/details3d.js +4342 -0
- package/public/js/doom.js +31 -0
- package/public/js/dosaudio.js +48 -0
- package/public/js/dosgame.js +389 -0
- package/public/js/dosio.js +186 -0
- package/public/js/dospc.js +1353 -0
- package/public/js/dosworker.js +196 -0
- package/public/js/explorer.js +405 -0
- package/public/js/feepalette.js +149 -0
- package/public/js/fmt.js +162 -0
- package/public/js/goggles.js +886 -0
- package/public/js/kiosk.js +41 -0
- package/public/js/login.js +88 -0
- package/public/js/markets.js +395 -0
- package/public/js/mining.js +1416 -0
- package/public/js/panels.js +970 -0
- package/public/js/pricechart.js +189 -0
- package/public/js/quake.js +20 -0
- package/public/js/settings.js +1096 -0
- package/public/js/soundcard.js +459 -0
- package/public/js/tetris.js +226 -0
- package/public/js/tetrust.js +356 -0
- package/public/js/tetsound.js +175 -0
- package/public/js/theme.js +235 -0
- package/public/js/wolf3d.js +22 -0
- package/public/js/x86.js +1978 -0
- package/public/login.html +33 -0
- package/scripts/blockfile-measure.js +156 -0
- package/scripts/browser-check.mjs +286 -0
- package/scripts/check.js +173 -0
- package/scripts/decode-check.js +81 -0
- package/scripts/doc-counts.js +109 -0
- package/scripts/donate-qr.py +23 -0
- package/scripts/dos-bench.js +56 -0
- package/scripts/fake-node.js +534 -0
- package/scripts/index-bench.js +216 -0
- package/scripts/index-benchmark.js +117 -0
- package/scripts/index-build.js +40 -0
- package/scripts/live-render-check.mjs +89 -0
- package/scripts/manage-users.js +132 -0
- package/scripts/motion-check.mjs +138 -0
- package/scripts/pool-map.js +157 -0
- package/scripts/setup.js +432 -0
- package/scripts/shots.mjs +278 -0
- package/scripts/smoke.sh +327 -0
- package/scripts/tls.js +31 -0
- package/scripts/ui.js +174 -0
- package/server/auth/sessions.js +221 -0
- package/server/auth/users.js +243 -0
- package/server/chain/blockfile.js +234 -0
- package/server/chain/index/build.js +210 -0
- package/server/chain/index/heights.js +36 -0
- package/server/chain/index/live.js +276 -0
- package/server/chain/index/rows.js +145 -0
- package/server/chain/index/store.js +154 -0
- package/server/chain/index/worker.js +109 -0
- package/server/chain/tx.js +310 -0
- package/server/collect/gbt.js +229 -0
- package/server/collect/logparse.js +765 -0
- package/server/collect/logtail.js +189 -0
- package/server/collect/markets.js +333 -0
- package/server/collect/mining.js +333 -0
- package/server/collect/monitor.js +2545 -0
- package/server/collect/network.js +295 -0
- package/server/collect/nextblock.js +275 -0
- package/server/collect/sync.js +386 -0
- package/server/config.js +644 -0
- package/server/http/api.js +1319 -0
- package/server/http/explorer.js +418 -0
- package/server/http/games.js +77 -0
- package/server/http/server.js +420 -0
- package/server/http/sse.js +176 -0
- package/server/http/static.js +212 -0
- package/server/main.js +673 -0
- package/server/netinfo.js +253 -0
- package/server/rpc/allowlist.js +130 -0
- package/server/rpc/client.js +414 -0
- package/server/store/audit.js +148 -0
- package/server/store/history.js +220 -0
- package/server/store/ledger.js +290 -0
- package/server/store/ring.js +173 -0
- package/server/tls/selfsigned.js +160 -0
- package/server/util/fmt.js +29 -0
- package/systemd/blockyard.service +102 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// feepalette.js -- feerate bands and their colours, for the block-space
|
|
4
|
+
// viewer, the block meter and the feerate legend.
|
|
5
|
+
//
|
|
6
|
+
// THE BANDS are a geometric series: from FLOOR_RATE to TOP_RATE in equal
|
|
7
|
+
// ratios, each threshold rounded to two significant figures so the legend
|
|
8
|
+
// reads in plain numbers. Everything below FLOOR_RATE is band 0. A geometric
|
|
9
|
+
// series is what a feerate scale wants: the difference between 1 and 2 sat/vB
|
|
10
|
+
// matters as much as the difference between 500 and 1000, so the bands sit
|
|
11
|
+
// close together near the floor and far apart near the ceiling.
|
|
12
|
+
//
|
|
13
|
+
// 128 BANDS FROM 0.1 sat/vB (operator, 2026-09-11: "we really need more color
|
|
14
|
+
// separation. at least 64 different colors and shades", then "we should try
|
|
15
|
+
// 128 colors"). The first palette began at 1 sat/vB with 36 bands, and this
|
|
16
|
+
// chain's blocks run from 0.1 to a few tens of sat/vB -- so most of a block
|
|
17
|
+
// fell into band 0 and the board was one green. Starting at 0.1 puts
|
|
18
|
+
// 0.1..30 sat/vB across some 73 bands, each step about 8% up the scale.
|
|
19
|
+
// Thresholds are two significant figures, three where two would repeat the
|
|
20
|
+
// band below (an 8% step is finer than two figures near the start of a decade).
|
|
21
|
+
//
|
|
22
|
+
// THE COLOURS are a ramp through a handful of anchors, interpolated in HSL
|
|
23
|
+
// with the hue unwrapped so it only ever travels one way: sky blue for the
|
|
24
|
+
// cheapest, through teal, green, yellow-green, yellow, amber, orange, red and
|
|
25
|
+
// crimson, to purple for the most expensive -- 280 degrees of hue. Band i takes
|
|
26
|
+
// the colour at i / (bands - 1) along the ramp, and its lightness steps
|
|
27
|
+
// through a three-band pattern (LIGHT_STEP), so two neighbouring bands differ
|
|
28
|
+
// in tone as well as hue and read as separate colours side by side.
|
|
29
|
+
//
|
|
30
|
+
// feeShade() then gives each tile its own tone of its band's colour: darker
|
|
31
|
+
// at the bottom of the band, brighter toward its top, plus a small fixed
|
|
32
|
+
// variation per transaction id so a field of equal feerates reads as a field
|
|
33
|
+
// of separate stones rather than one flat slab. Only the brightness moves;
|
|
34
|
+
// the hue is the band's.
|
|
35
|
+
|
|
36
|
+
const FLOOR_RATE = 0.1; // sat/vB: the start of band 1 (below it is band 0)
|
|
37
|
+
const TOP_RATE = 2000; // sat/vB: the start of the last band
|
|
38
|
+
const STEPS = 126; // ratios between FLOOR_RATE and TOP_RATE: 128 bands in all
|
|
39
|
+
|
|
40
|
+
// n significant figures: figures(1.2505, 2) -> 1.3, figures(14.6, 2) -> 15, figures(1.049, 3) -> 1.05
|
|
41
|
+
function figures(v, n) {
|
|
42
|
+
const mag = 10 ** (Math.floor(Math.log10(v)) - (n - 1));
|
|
43
|
+
return Math.round(v / mag) * mag;
|
|
44
|
+
}
|
|
45
|
+
const tidy = (v) => Number(v.toPrecision(12)); // drop float dust (1.3000000000000003)
|
|
46
|
+
|
|
47
|
+
export const FEE_BANDS = Object.freeze((() => {
|
|
48
|
+
const bands = [0];
|
|
49
|
+
for (let i = 0; i <= STEPS; i++) {
|
|
50
|
+
const v = FLOOR_RATE * (TOP_RATE / FLOOR_RATE) ** (i / STEPS);
|
|
51
|
+
const two = tidy(figures(v, 2));
|
|
52
|
+
bands.push(two > bands.at(-1) ? two : tidy(figures(v, 3)));
|
|
53
|
+
}
|
|
54
|
+
return bands;
|
|
55
|
+
})());
|
|
56
|
+
|
|
57
|
+
// [position along the ramp, hue (deg), saturation, lightness], with the rate
|
|
58
|
+
// that lands there. Hues are unwrapped (they fall from blue past 0 into
|
|
59
|
+
// negative = crimson/purple), so linear interpolation moves the hue one way.
|
|
60
|
+
const ANCHORS = [
|
|
61
|
+
[0.00, 205, 0.62, 0.42], // sky blue under 0.1 sat/vB
|
|
62
|
+
[0.12, 172, 0.60, 0.40], // teal ~0.3
|
|
63
|
+
[0.24, 138, 0.58, 0.40], // green ~1
|
|
64
|
+
[0.35, 96, 0.58, 0.45], // yellow-green ~3
|
|
65
|
+
[0.47, 58, 0.72, 0.50], // yellow ~10
|
|
66
|
+
[0.56, 40, 0.76, 0.51], // amber ~25
|
|
67
|
+
[0.65, 24, 0.78, 0.51], // orange ~60
|
|
68
|
+
[0.78, 2, 0.72, 0.50], // red ~250
|
|
69
|
+
[0.89, -28, 0.70, 0.46], // crimson ~700
|
|
70
|
+
[1.00, -75, 0.60, 0.46], // purple 2000 and up
|
|
71
|
+
];
|
|
72
|
+
// lightness added band by band, in a repeating three-step pattern
|
|
73
|
+
const LIGHT_STEP = [0, 0.075, -0.06];
|
|
74
|
+
|
|
75
|
+
function hslToRgb(h, s, l) {
|
|
76
|
+
const hh = ((h % 360) + 360) % 360;
|
|
77
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
78
|
+
const x = c * (1 - Math.abs(((hh / 60) % 2) - 1));
|
|
79
|
+
const m = l - c / 2;
|
|
80
|
+
const [r, g, b] = hh < 60 ? [c, x, 0] : hh < 120 ? [x, c, 0] : hh < 180 ? [0, c, x]
|
|
81
|
+
: hh < 240 ? [0, x, c] : hh < 300 ? [x, 0, c] : [c, 0, x];
|
|
82
|
+
return [r + m, g + m, b + m].map((v) => Math.round(v * 255));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function rampAt(t, dl = 0) {
|
|
86
|
+
let i = 0;
|
|
87
|
+
while (i < ANCHORS.length - 2 && t > ANCHORS[i + 1][0]) i++;
|
|
88
|
+
const [t0, h0, s0, l0] = ANCHORS[i];
|
|
89
|
+
const [t1, h1, s1, l1] = ANCHORS[i + 1];
|
|
90
|
+
const f = Math.min(1, Math.max(0, (t - t0) / (t1 - t0)));
|
|
91
|
+
return hslToRgb(h0 + (h1 - h0) * f, s0 + (s1 - s0) * f, l0 + (l1 - l0) * f + dl);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hex = (rgb) => '#' + rgb.map((v) => Math.min(255, Math.max(0, Math.round(v))).toString(16).padStart(2, '0')).join('');
|
|
95
|
+
|
|
96
|
+
const BAND_RGB = FEE_BANDS.map((_, i) => rampAt(i / (FEE_BANDS.length - 1), LIGHT_STEP[i % LIGHT_STEP.length]));
|
|
97
|
+
const BAND_HEX = BAND_RGB.map(hex);
|
|
98
|
+
|
|
99
|
+
export function feeBandIndex(rate) {
|
|
100
|
+
const r = Number(rate);
|
|
101
|
+
if (!Number.isFinite(r) || r <= 0) return 0;
|
|
102
|
+
// binary search: the highest band whose threshold is <= r
|
|
103
|
+
let lo = 0, hi = FEE_BANDS.length - 1;
|
|
104
|
+
while (lo < hi) {
|
|
105
|
+
const mid = (lo + hi + 1) >> 1;
|
|
106
|
+
if (FEE_BANDS[mid] <= r) lo = mid; else hi = mid - 1;
|
|
107
|
+
}
|
|
108
|
+
return lo;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function feeColor(rate) {
|
|
112
|
+
return BAND_HEX[feeBandIndex(rate)];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Where a rate sits inside its band, 0 at the threshold and 1 at the next.
|
|
116
|
+
// The last band has no upper threshold; it is given one as wide as itself.
|
|
117
|
+
function inBand(rate, i) {
|
|
118
|
+
const r = Number(rate);
|
|
119
|
+
if (!Number.isFinite(r) || r <= 0) return 0;
|
|
120
|
+
const lo = FEE_BANDS[i];
|
|
121
|
+
const hi = i + 1 < FEE_BANDS.length ? FEE_BANDS[i + 1] : lo * 2;
|
|
122
|
+
return Math.min(1, Math.max(0, (r - lo) / (hi - lo)));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// FNV-1a, 32 bits: a stable number per key, the same in every browser
|
|
126
|
+
function hashKey(key) {
|
|
127
|
+
let h = 0x811c9dc5;
|
|
128
|
+
for (let i = 0; i < key.length; i++) {
|
|
129
|
+
h ^= key.charCodeAt(i);
|
|
130
|
+
h = Math.imul(h, 0x01000193);
|
|
131
|
+
}
|
|
132
|
+
return h >>> 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const SHADE_LOW = 0.84; // brightness at the bottom of a band
|
|
136
|
+
const SHADE_SPAN = 0.26; // added across the band (so 1.10 at its top)
|
|
137
|
+
const KEY_SPREAD = 0.06; // +/- per-key variation
|
|
138
|
+
|
|
139
|
+
export function feeShade(rate, key = '') {
|
|
140
|
+
const i = feeBandIndex(rate);
|
|
141
|
+
const rgb = BAND_RGB[i];
|
|
142
|
+
let k = SHADE_LOW + SHADE_SPAN * inBand(rate, i);
|
|
143
|
+
const id = key == null ? '' : String(key);
|
|
144
|
+
if (id) k += KEY_SPREAD * ((hashKey(id) % 1001) / 500 - 1);
|
|
145
|
+
// never past full brightness in any channel: clipping one channel would
|
|
146
|
+
// change the hue, and the hue is the band's
|
|
147
|
+
k = Math.min(k, 255 / Math.max(1, ...rgb));
|
|
148
|
+
return hex(rgb.map((v) => v * k));
|
|
149
|
+
}
|
package/public/js/fmt.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Display formatters. Units are decimal (1 KB = 1000 B) to match how the node
|
|
2
|
+
// itself prints sizes -- its log renders 4096 bytes as "4.0KB", so the monitor
|
|
3
|
+
// must not be the one place on the box that says 4.1 KB.
|
|
4
|
+
const U = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
5
|
+
|
|
6
|
+
export function bytes(n, dp = 1) {
|
|
7
|
+
if (n == null || !Number.isFinite(n)) return '–';
|
|
8
|
+
let v = Math.abs(n);
|
|
9
|
+
let i = 0;
|
|
10
|
+
while (v >= 1000 && i < U.length - 1) { v /= 1000; i += 1; }
|
|
11
|
+
return `${n < 0 ? '-' : ''}${v.toFixed(i === 0 ? 0 : dp)} ${U[i]}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function rate(bps, unit = 'B/s') {
|
|
15
|
+
if (bps == null || !Number.isFinite(bps)) return '–';
|
|
16
|
+
let v = Math.abs(bps);
|
|
17
|
+
let i = 0;
|
|
18
|
+
while (v >= 1000 && i < U.length - 1) { v /= 1000; i += 1; }
|
|
19
|
+
return `${(bps < 0 ? '-' : '')}${v.toFixed(i === 0 ? 0 : 1)} ${U[i]}/${unit === 'B/s' ? 's' : unit}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function num(n, dp = 0) {
|
|
23
|
+
if (n == null || !Number.isFinite(n)) return '–';
|
|
24
|
+
return n.toLocaleString('en-US', { minimumFractionDigits: dp, maximumFractionDigits: dp });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function pct(n, dp = 2) {
|
|
28
|
+
if (n == null || !Number.isFinite(n)) return '–';
|
|
29
|
+
return `${n.toFixed(dp)}%`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// DD:HH:MM:SS, matching the node's own ETA and heartbeat format.
|
|
33
|
+
export function eta(sec) {
|
|
34
|
+
if (sec == null || !Number.isFinite(sec) || sec < 0) return '–';
|
|
35
|
+
const s = Math.floor(sec);
|
|
36
|
+
const p = (x, w = 2) => String(x).padStart(w, '0');
|
|
37
|
+
return `${p(Math.floor(s / 86400), 2)}:${p(Math.floor((s % 86400) / 3600))}:${p(Math.floor((s % 3600) / 60))}:${p(s % 60)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ago(ms, now = Date.now()) {
|
|
41
|
+
if (ms == null || !Number.isFinite(ms)) return '–';
|
|
42
|
+
const s = Math.max(0, (now - ms) / 1000);
|
|
43
|
+
if (s < 45) return `${Math.round(s)}s ago`;
|
|
44
|
+
if (s < 5400) return `${Math.round(s / 60)}m ago`;
|
|
45
|
+
if (s < 172800) return `${(s / 3600).toFixed(1)}h ago`;
|
|
46
|
+
return `${(s / 86400).toFixed(1)}d ago`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function ageSec(sec) {
|
|
50
|
+
if (sec == null || !Number.isFinite(sec)) return '–';
|
|
51
|
+
if (sec < 60) return `${Math.round(sec)}s`;
|
|
52
|
+
if (sec < 5400) return `${(sec / 60).toFixed(1)}m`;
|
|
53
|
+
if (sec < 172800) return `${(sec / 3600).toFixed(1)}h`;
|
|
54
|
+
return `${(sec / 86400).toFixed(1)}d`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function sats(n) {
|
|
58
|
+
if (n == null || !Number.isFinite(n)) return '–';
|
|
59
|
+
return n.toLocaleString('en-US');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function btc(sat) {
|
|
63
|
+
if (sat == null || !Number.isFinite(sat)) return '–';
|
|
64
|
+
return (sat / 1e8).toFixed(8);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Core reports feerates in BTC/kvB; operators think in sat/vB. Convert, and show
|
|
68
|
+
// the unit every time so nobody compares two numbers in different currencies.
|
|
69
|
+
export function satPerVb(btcPerKvB, dp = 2) {
|
|
70
|
+
if (btcPerKvB == null || !Number.isFinite(btcPerKvB)) return '–';
|
|
71
|
+
return (btcPerKvB * 1e8 / 1000).toFixed(dp);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// `n` is already in EH/s, so each step up is a THOUSAND of them: 1e3 EH/s is a zettahash and 1e6
|
|
75
|
+
// is a yottahash. Both labels were one prefix too low (2026-09-12) -- 1112 EH/s printed as
|
|
76
|
+
// "1.11 EH/s" -- which stayed hidden while the estimator that feeds this was itself out by 2^32
|
|
77
|
+
// and never produced a number above 1.
|
|
78
|
+
export function eh(n) {
|
|
79
|
+
if (n == null || !Number.isFinite(n)) return '–';
|
|
80
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(2)} YH/s`;
|
|
81
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(2)} ZH/s`;
|
|
82
|
+
return `${n.toFixed(1)} EH/s`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function clock(ms) {
|
|
86
|
+
const d = new Date(ms);
|
|
87
|
+
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
export function short(n) {
|
|
92
|
+
if (n == null || !Number.isFinite(n)) return '–';
|
|
93
|
+
const a = Math.abs(n);
|
|
94
|
+
if (a >= 1e12) return (n / 1e12).toFixed(1) + 'T';
|
|
95
|
+
if (a >= 1e9) return (n / 1e9).toFixed(1) + 'G';
|
|
96
|
+
if (a >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
97
|
+
if (a >= 1e3) return (n / 1e3).toFixed(a >= 1e4 ? 0 : 1) + 'k';
|
|
98
|
+
if (a >= 100) return n.toFixed(0);
|
|
99
|
+
if (a >= 1) return (+n.toFixed(1)).toString();
|
|
100
|
+
return (+n.toFixed(4)).toString();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function hash(x, keep = 8) {
|
|
104
|
+
if (!x || typeof x !== 'string') return '–';
|
|
105
|
+
if (x.length <= keep * 2 + 3) return x;
|
|
106
|
+
return `${x.slice(0, keep)}…${x.slice(-keep)}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function esc(s) {
|
|
110
|
+
return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export function uptime(ms) {
|
|
114
|
+
if (!Number.isFinite(ms)) return '–';
|
|
115
|
+
const s = Math.floor(ms / 1000);
|
|
116
|
+
const d = Math.floor(s / 86400);
|
|
117
|
+
const h = Math.floor((s % 86400) / 3600);
|
|
118
|
+
const m = Math.floor((s % 3600) / 60);
|
|
119
|
+
return d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// THE ONE WARNING THE BANNER DOES NOT SHOW (operator, 2026-09-13, getting ready for release:
|
|
123
|
+
// "how do we get rid of the node warning?" -- "This is a pre-release test build - use at your
|
|
124
|
+
// own risk - do not use for mining or merchant applications").
|
|
125
|
+
//
|
|
126
|
+
// It is TRUE: a node built from source reports itself pre-release on every poll, which means a red
|
|
127
|
+
// caveat sits across the hero permanently and stops meaning anything. The fix is deliberately
|
|
128
|
+
// the narrowest one available -- this exact notice, matched on the phrase bitcoind has always
|
|
129
|
+
// used for it, and nothing else. "unknown new rules activated", a chain reorganisation, an
|
|
130
|
+
// unsupported chainstate: all still land in the banner, in red.
|
|
131
|
+
//
|
|
132
|
+
// WHERE THIS DOES NOT HAPPEN: the server. monitor.js and sync.js keep passing the node's exact
|
|
133
|
+
// words through, so /api/state still reports what the node said and the filter cannot become a
|
|
134
|
+
// way for the monitor to conceal it. This is a presentation choice, applied at the banner.
|
|
135
|
+
const PRE_RELEASE = /pre-release test build/i;
|
|
136
|
+
|
|
137
|
+
/** The node's warnings, minus the permanent pre-release notice. Never throws on odd input. */
|
|
138
|
+
export function nodeWarnings(list) {
|
|
139
|
+
if (!Array.isArray(list)) return [];
|
|
140
|
+
return list.filter((w) => typeof w === 'string' && w.trim() && !PRE_RELEASE.test(w));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Copy text to the clipboard. The monitor is served over plain http on the LAN, where
|
|
145
|
+
* navigator.clipboard does not exist (it needs a secure context), so a select-and-copy fallback is
|
|
146
|
+
* kept. Resolves when copied, rejects when the browser refused. (public/js/explorer.js keeps its
|
|
147
|
+
* own copy of this: that file imports nothing by design.)
|
|
148
|
+
*/
|
|
149
|
+
export function copyText(text) {
|
|
150
|
+
if (globalThis.navigator?.clipboard && globalThis.isSecureContext) return navigator.clipboard.writeText(text);
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
const ta = document.createElement('textarea');
|
|
153
|
+
ta.value = text;
|
|
154
|
+
ta.setAttribute('readonly', '');
|
|
155
|
+
ta.className = 'xclip';
|
|
156
|
+
document.body.appendChild(ta);
|
|
157
|
+
ta.select();
|
|
158
|
+
const ok = document.execCommand?.('copy');
|
|
159
|
+
ta.remove();
|
|
160
|
+
if (ok) resolve(); else reject(new Error('copy refused'));
|
|
161
|
+
});
|
|
162
|
+
}
|