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.
Files changed (122) hide show
  1. package/CHANGELOG.md +929 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +4 -0
  4. package/README.md +191 -4
  5. package/SECURITY.md +38 -0
  6. package/bin/blockyard.js +41 -0
  7. package/config/pool-map.json +2620 -0
  8. package/docs/API.md +1577 -0
  9. package/docs/ARCHITECTURE.md +1394 -0
  10. package/docs/AUTO-UPDATE.md +269 -0
  11. package/docs/CONFIGURATION.md +847 -0
  12. package/docs/DEFECTS.md +813 -0
  13. package/docs/EFFECTS-AGENTS.md +448 -0
  14. package/docs/GETTING-STARTED.md +205 -0
  15. package/docs/INSTALL.md +547 -0
  16. package/docs/MEASUREMENTS.md +1401 -0
  17. package/docs/RULES.md +681 -0
  18. package/docs/SECURITY-AUDIT-2026-09-14.md +177 -0
  19. package/docs/SECURITY-AUDIT.md +258 -0
  20. package/docs/SECURITY.md +212 -0
  21. package/docs/TROUBLESHOOTING.md +332 -0
  22. package/docs/USER-GUIDE.md +1262 -0
  23. package/package.json +53 -5
  24. package/public/404.html +9 -0
  25. package/public/css/app.css +2009 -0
  26. package/public/donate-qr.png +0 -0
  27. package/public/index.html +1085 -0
  28. package/public/js/about.js +112 -0
  29. package/public/js/agents.js +1141 -0
  30. package/public/js/app.js +1386 -0
  31. package/public/js/arkanoid.js +806 -0
  32. package/public/js/blockanoid.js +347 -0
  33. package/public/js/blockout.js +347 -0
  34. package/public/js/blockpack.js +428 -0
  35. package/public/js/blockscene3d.js +2830 -0
  36. package/public/js/breakout.js +224 -0
  37. package/public/js/charts.js +635 -0
  38. package/public/js/depthchart.js +315 -0
  39. package/public/js/details3d.js +4342 -0
  40. package/public/js/doom.js +31 -0
  41. package/public/js/dosaudio.js +48 -0
  42. package/public/js/dosgame.js +389 -0
  43. package/public/js/dosio.js +186 -0
  44. package/public/js/dospc.js +1353 -0
  45. package/public/js/dosworker.js +196 -0
  46. package/public/js/explorer.js +405 -0
  47. package/public/js/feepalette.js +149 -0
  48. package/public/js/fmt.js +162 -0
  49. package/public/js/goggles.js +886 -0
  50. package/public/js/kiosk.js +41 -0
  51. package/public/js/login.js +88 -0
  52. package/public/js/markets.js +395 -0
  53. package/public/js/mining.js +1416 -0
  54. package/public/js/panels.js +970 -0
  55. package/public/js/pricechart.js +189 -0
  56. package/public/js/quake.js +20 -0
  57. package/public/js/settings.js +1096 -0
  58. package/public/js/soundcard.js +459 -0
  59. package/public/js/tetris.js +226 -0
  60. package/public/js/tetrust.js +356 -0
  61. package/public/js/tetsound.js +175 -0
  62. package/public/js/theme.js +235 -0
  63. package/public/js/wolf3d.js +22 -0
  64. package/public/js/x86.js +1978 -0
  65. package/public/login.html +33 -0
  66. package/scripts/blockfile-measure.js +156 -0
  67. package/scripts/browser-check.mjs +286 -0
  68. package/scripts/check.js +173 -0
  69. package/scripts/decode-check.js +81 -0
  70. package/scripts/doc-counts.js +109 -0
  71. package/scripts/donate-qr.py +23 -0
  72. package/scripts/dos-bench.js +56 -0
  73. package/scripts/fake-node.js +534 -0
  74. package/scripts/index-bench.js +216 -0
  75. package/scripts/index-benchmark.js +117 -0
  76. package/scripts/index-build.js +40 -0
  77. package/scripts/live-render-check.mjs +89 -0
  78. package/scripts/manage-users.js +132 -0
  79. package/scripts/motion-check.mjs +138 -0
  80. package/scripts/pool-map.js +157 -0
  81. package/scripts/setup.js +432 -0
  82. package/scripts/shots.mjs +278 -0
  83. package/scripts/smoke.sh +327 -0
  84. package/scripts/tls.js +31 -0
  85. package/scripts/ui.js +174 -0
  86. package/server/auth/sessions.js +221 -0
  87. package/server/auth/users.js +243 -0
  88. package/server/chain/blockfile.js +234 -0
  89. package/server/chain/index/build.js +210 -0
  90. package/server/chain/index/heights.js +36 -0
  91. package/server/chain/index/live.js +276 -0
  92. package/server/chain/index/rows.js +145 -0
  93. package/server/chain/index/store.js +154 -0
  94. package/server/chain/index/worker.js +109 -0
  95. package/server/chain/tx.js +310 -0
  96. package/server/collect/gbt.js +229 -0
  97. package/server/collect/logparse.js +765 -0
  98. package/server/collect/logtail.js +189 -0
  99. package/server/collect/markets.js +333 -0
  100. package/server/collect/mining.js +333 -0
  101. package/server/collect/monitor.js +2545 -0
  102. package/server/collect/network.js +295 -0
  103. package/server/collect/nextblock.js +275 -0
  104. package/server/collect/sync.js +386 -0
  105. package/server/config.js +644 -0
  106. package/server/http/api.js +1319 -0
  107. package/server/http/explorer.js +418 -0
  108. package/server/http/games.js +77 -0
  109. package/server/http/server.js +420 -0
  110. package/server/http/sse.js +176 -0
  111. package/server/http/static.js +212 -0
  112. package/server/main.js +673 -0
  113. package/server/netinfo.js +253 -0
  114. package/server/rpc/allowlist.js +130 -0
  115. package/server/rpc/client.js +414 -0
  116. package/server/store/audit.js +148 -0
  117. package/server/store/history.js +220 -0
  118. package/server/store/ledger.js +290 -0
  119. package/server/store/ring.js +173 -0
  120. package/server/tls/selfsigned.js +160 -0
  121. package/server/util/fmt.js +29 -0
  122. package/systemd/blockyard.service +102 -0
@@ -0,0 +1,315 @@
1
+ // THE DEPTH CHART (operator, 2026-09-11, with a picture of bitcoinity's: "we are totally missing
2
+ // this view in the market info").
3
+ //
4
+ // x is price. The left axis is cumulative BTC: bids (green) summed downward from the best bid,
5
+ // asks (red) summed upward from the best ask -- every exchange's book a faint line, all of them
6
+ // together the bright line, and that total as it stood N minutes ago the dashed line. The right
7
+ // axis is a symmetric log scale, as on bitcoinity, for the change bars: how much was added (+) or
8
+ // pulled (-) AT each price since then. The books come from server/collect/markets.js, read every
9
+ // 30 s while the tab is open, as cumulative depth on fixed $50 levels. A shallow book (Kraken
10
+ // gives 500 levels, OKX 5000 -- a percent or two either side) is not extended: its line ends,
11
+ // the end is marked, and the total past it counts only the books that reach.
12
+ import { niceTicks } from './charts.js';
13
+ import { INK } from './theme.js'; // the axis, grid and tooltip colours of the chosen theme
14
+ import { prep } from './pricechart.js';
15
+
16
+ export const AGOS = [[60, '1m'], [300, '5m'], [600, '10m'], [1800, '30m'], [3600, '1h']];
17
+ export const ZOOMS = [[0.01, '±1%'], [0.025, '±2.5%'], [0.05, '±5%'], [0.1, '±10%']];
18
+ export const DEPTH_MS = 30_000;
19
+ const D = { data: null, at: 0, busy: false, key: null, ago: 600, zoom: 0.05, hover: null, bound: false, error: null };
20
+ // the two sides' colours come from the theme's ink (theme.js, 2026-09-16): neon green and red on a
21
+ // dark ground, the theme's own good and bad on a light one, where neon has no contrast
22
+ const PAD_FULL = { top: 28, right: 62, bottom: 22, left: 56 };
23
+ // COMPACT, for the kiosk. The full chart spends 62 px on the right purely to label the change
24
+ // bars' axis and 56 on the left for the cumulative-BTC labels plus a rotated caption -- on a wall
25
+ // panel a couple of hundred pixels tall that is most of the picture. Compact keeps the curves and
26
+ // the price axis, which are what carry the meaning across a room, and drops the annotation.
27
+ const PAD_COMPACT = { top: 15, right: 10, bottom: 17, left: 42 };
28
+
29
+ const money = (v, dp = 0) => (v == null ? '–' : v.toLocaleString('en-US', { minimumFractionDigits: dp, maximumFractionDigits: dp }));
30
+ const btc = (v) => (v == null ? '–' : Math.abs(v) >= 100 ? Math.round(v).toLocaleString('en-US') : v.toFixed(Math.abs(v) >= 10 ? 1 : 2));
31
+ const kUsd = (v) => `${(v / 1000).toFixed(v % 1000 ? 1 : 0)}k`;
32
+ const shortN = (v) => (v >= 1000 ? `${v / 1000}k` : String(v));
33
+ const clock = (t) => new Date(t).toISOString().slice(11, 19);
34
+
35
+ // Pure: the /api/markets/depth reply in, the curves the chart draws out.
36
+ export function depthSeries(d) {
37
+ if (!d?.ok || !d.n || !(d.exchanges ?? []).some((e) => e.bids)) return null;
38
+ const n = d.n;
39
+ const prices = Array.from({ length: n }, (_, i) => d.p0 + i * d.step);
40
+ // A BOOK THAT ENDS STILL COUNTS (2026-09-11: with Kraken's 500 levels ending at -2%, the total
41
+ // DROPPED there, and "within 5%" printed smaller than "within 1%"). Cumulative depth past the
42
+ // last level a book gave is at least what it had reached, so that value is carried on outward
43
+ // as a lower bound -- not an extrapolation -- and those levels are flagged partial: the chart
44
+ // draws the total there as "at least", and the readout says so.
45
+ const carry = (arr, side) => {
46
+ if (!arr) return null;
47
+ const o = arr.slice();
48
+ if (side === 'bid') { const lo = o.findIndex((v) => v != null); if (lo > 0) for (let i = lo - 1; i >= 0; i--) o[i] = o[lo]; }
49
+ else { let hi = -1; for (let i = n - 1; i >= 0; i--) if (o[i] != null) { hi = i; break; } if (hi >= 0) for (let i = hi + 1; i < n; i++) o[i] = o[hi]; }
50
+ return o;
51
+ };
52
+ const sum = (key, side) => {
53
+ const tot = new Array(n).fill(null), part = new Array(n).fill(false);
54
+ for (const e of d.exchanges) {
55
+ const raw = e[key];
56
+ const full = carry(raw, side);
57
+ if (!full) continue;
58
+ for (let i = 0; i < n; i++) {
59
+ if (full[i] == null) continue;
60
+ tot[i] = (tot[i] ?? 0) + full[i];
61
+ if (raw[i] == null) part[i] = true;
62
+ }
63
+ }
64
+ return { tot, part };
65
+ };
66
+ const B = sum('bids', 'bid'), A = sum('asks', 'ask'), TB = sum('thenBids', 'bid'), TA = sum('thenAsks', 'ask');
67
+ const bid = B.tot, ask = A.tot, thenBid = TB.tot, thenAsk = TA.tot;
68
+ const bidPart = B.part, askPart = A.part;
69
+ const hasThen = d.exchanges.some((e) => e.thenBids);
70
+ // the amount AT a level (not cumulative): bids accumulate downward, asks upward
71
+ const amt = (cum, i, side) => (cum[i] == null ? null : cum[i] - ((side === 'bid' ? cum[i + 1] : cum[i - 1]) ?? 0));
72
+ const change = hasThen ? prices.map((_, i) => {
73
+ const nb = amt(bid, i, 'bid'), tb = amt(thenBid, i, 'bid'), na = amt(ask, i, 'ask'), ta = amt(thenAsk, i, 'ask');
74
+ const v = (nb != null && tb != null && !bidPart[i] ? nb - tb : 0) + (na != null && ta != null && !askPart[i] ? na - ta : 0);
75
+ return Math.abs(v) < 1e-6 ? 0 : v;
76
+ }) : null;
77
+ const reach = d.exchanges.filter((e) => e.bids).map((e) => {
78
+ const lo = e.bids.findIndex((v) => v != null);
79
+ let hi = -1;
80
+ for (let i = n - 1; i >= 0; i--) if (e.asks?.[i] != null) { hi = i; break; }
81
+ return { id: e.id, name: e.name, low: lo > 0 ? prices[lo] : null, high: hi >= 0 && hi < n - 1 ? prices[hi] : null };
82
+ });
83
+ return { prices, step: d.step, bid, ask, bidPart, askPart, thenBid, thenAsk, change, mid: d.mid, exchanges: d.exchanges.filter((e) => e.bids), reach };
84
+ }
85
+
86
+ // Cumulative BTC bid within pct below the mid, and asked within pct above it.
87
+ export function depthSums(ser, pct) {
88
+ const idx = (p) => Math.round((p - ser.prices[0]) / ser.step);
89
+ const ib = idx(ser.mid * (1 - pct)), ia = idx(ser.mid * (1 + pct));
90
+ return { bids: ser.bid[ib] ?? null, asks: ser.ask[ia] ?? null, atLeast: Boolean(ser.bidPart?.[ib] || ser.askPart?.[ia]) };
91
+ }
92
+
93
+ export function drawDepth(canvas, ser, { zoom = 0.05, hover = null, compact = false, empty = null } = {}) {
94
+ if (!canvas?.getContext) return null;
95
+ const PAD = compact ? PAD_COMPACT : PAD_FULL;
96
+ const { ctx, w, h } = prep(canvas);
97
+ if (!ser) {
98
+ ctx.fillStyle = INK.text; ctx.textAlign = 'center';
99
+ ctx.fillText(empty ?? 'asking the exchanges for their order books…', w / 2, h / 2);
100
+ return null;
101
+ }
102
+ const plotW = Math.max(10, w - PAD.left - PAD.right), plotH = Math.max(40, h - PAD.top - PAD.bottom);
103
+ const pmin = ser.mid * (1 - zoom), pmax = ser.mid * (1 + zoom);
104
+ const X = (p) => PAD.left + ((p - pmin) / (pmax - pmin)) * plotW;
105
+ const inWin = (i) => ser.prices[i] >= pmin && ser.prices[i] <= pmax;
106
+ const line = (x0, y0, x1, y1) => { ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); };
107
+ let vmax = 1;
108
+ ser.prices.forEach((_, i) => { if (inWin(i)) for (const a of [ser.bid, ser.ask, ser.thenBid, ser.thenAsk]) if (a[i] != null && a[i] > vmax) vmax = a[i]; });
109
+ vmax *= 1.06;
110
+ const Y = (v) => PAD.top + plotH - (v / vmax) * plotH;
111
+ let cmax = 1;
112
+ if (ser.change) ser.change.forEach((v, i) => { if (inWin(i) && Math.abs(v) > cmax) cmax = Math.abs(v); });
113
+ const F = Math.max(1, Math.ceil(Math.log10(1 + cmax)));
114
+ const sl = (v) => Math.sign(v) * Math.log10(1 + Math.abs(v));
115
+ const YC = (v) => PAD.top + plotH / 2 - (sl(v) / F) * (plotH / 2);
116
+
117
+ // left axis: cumulative BTC
118
+ ctx.lineWidth = 1;
119
+ ctx.textAlign = 'right';
120
+ for (const v of niceTicks(0, vmax, Math.max(3, Math.floor(plotH / 44)))) {
121
+ const y = Math.round(Y(v)) + 0.5;
122
+ if (y < PAD.top) continue;
123
+ ctx.strokeStyle = INK.grid; line(PAD.left, y, w - PAD.right, y);
124
+ ctx.fillStyle = INK.axisText; ctx.fillText(btc(v), PAD.left - 6, y);
125
+ }
126
+ if (!compact) {
127
+ ctx.save?.();
128
+ ctx.translate(12, PAD.top + plotH / 2); ctx.rotate(-Math.PI / 2);
129
+ ctx.textAlign = 'center'; ctx.fillStyle = INK.textDim; ctx.fillText('BTC, cumulative', 0, 0);
130
+ ctx.restore?.();
131
+ }
132
+ ctx.setTransform(Math.min(globalThis.devicePixelRatio || 1, 2), 0, 0, Math.min(globalThis.devicePixelRatio || 1, 2), 0, 0);
133
+ // right axis: the change bars' symmetric log scale
134
+ if (ser.change) {
135
+ ctx.textAlign = 'left';
136
+ for (let e = -F; e <= F; e++) {
137
+ const v = e === 0 ? 0 : Math.sign(e) * 10 ** Math.abs(e);
138
+ const y = Math.round(YC(v)) + 0.5;
139
+ if (!compact) {
140
+ ctx.fillStyle = INK.axisText;
141
+ ctx.fillText(v === 0 ? '0' : `${v > 0 ? '' : '-'}${shortN(Math.abs(v))}`, w - PAD.right + 6, y);
142
+ }
143
+ if (v === 0) { ctx.strokeStyle = INK.axis; line(PAD.left, y, w - PAD.right, y); }
144
+ }
145
+ }
146
+ // price axis
147
+ ctx.textAlign = 'center';
148
+ for (const p of niceTicks(pmin, pmax, Math.max(3, Math.floor(plotW / 90)))) {
149
+ const x = Math.round(X(p)) + 0.5;
150
+ if (x < PAD.left || x > w - PAD.right) continue;
151
+ ctx.strokeStyle = INK.grid; line(x, PAD.top, x, PAD.top + plotH);
152
+ ctx.fillStyle = INK.axisText; ctx.fillText(zoom <= 0.01 ? money(p) : kUsd(p), x, h - PAD.bottom / 2);
153
+ }
154
+ ctx.strokeStyle = INK.axis;
155
+ line(PAD.left + 0.5, PAD.top, PAD.left + 0.5, PAD.top + plotH);
156
+ line(w - PAD.right + 0.5, PAD.top, w - PAD.right + 0.5, PAD.top + plotH);
157
+ line(PAD.left, PAD.top + plotH + 0.5, w - PAD.right, PAD.top + plotH + 0.5);
158
+
159
+ // change bars
160
+ if (ser.change) {
161
+ const bw = Math.max(1, (plotW / ((pmax - pmin) / ser.step)) * 0.8);
162
+ ser.change.forEach((v, i) => {
163
+ if (!v || !inWin(i)) return;
164
+ const x = X(ser.prices[i]), y0 = YC(0), y1 = YC(v);
165
+ ctx.fillStyle = v > 0 ? 'rgba(110,180,255,0.6)' : 'rgba(255,170,70,0.6)';
166
+ ctx.fillRect(x - bw / 2, Math.min(y0, y1), bw, Math.max(1, Math.abs(y1 - y0)));
167
+ });
168
+ }
169
+ // the curves: each book faint, the total then (dashed), the total now (bright)
170
+ const curve = (arr, col, lw, dash = []) => {
171
+ if (!arr) return;
172
+ ctx.strokeStyle = col; ctx.lineWidth = lw; ctx.setLineDash(dash);
173
+ ctx.beginPath();
174
+ let on = false;
175
+ ser.prices.forEach((p, i) => {
176
+ const v = arr[i];
177
+ if (v == null || !inWin(i)) { on = false; return; }
178
+ if (on) ctx.lineTo(X(p), Y(v)); else { ctx.moveTo(X(p), Y(v)); on = true; }
179
+ });
180
+ ctx.stroke();
181
+ ctx.setLineDash([]); ctx.lineWidth = 1;
182
+ };
183
+ for (const e of ser.exchanges) { curve(e.bids, INK.bidT, 1); curve(e.asks, INK.askT, 1); }
184
+ if (ser.change) { curve(ser.thenBid, INK.bidThen, 1.4, [5, 4]); curve(ser.thenAsk, INK.askThen, 1.4, [5, 4]); }
185
+ const only = (arr, part, want) => arr.map((v, i) => (v != null && (part[i] === want || (i > 0 && part[i - 1] === want) || (i < arr.length - 1 && part[i + 1] === want)) ? v : null));
186
+ curve(only(ser.bid, ser.bidPart, true), INK.bidHalf, 2.2, [2, 3]);
187
+ curve(only(ser.ask, ser.askPart, true), INK.askHalf, 2.2, [2, 3]);
188
+ curve(only(ser.bid, ser.bidPart, false), INK.bid, 2.2);
189
+ curve(only(ser.ask, ser.askPart, false), INK.ask, 2.2);
190
+ // the mid, and where each shallow book ends
191
+ ctx.strokeStyle = INK.dash;
192
+ const xm = Math.round(X(ser.mid)) + 0.5;
193
+ line(xm, PAD.top, xm, PAD.top + plotH);
194
+ ctx.font = '10px ui-monospace, SFMono-Regular, Menlo, monospace';
195
+ let lane = 0;
196
+ for (const r of ser.reach) {
197
+ for (const p of [r.low, r.high]) {
198
+ if (p == null || p < pmin || p > pmax) continue;
199
+ const x = Math.round(X(p)) + 0.5;
200
+ ctx.strokeStyle = INK.dash; line(x, PAD.top + plotH - 8, x, PAD.top + plotH);
201
+ if (!compact) {
202
+ ctx.fillStyle = INK.label; ctx.textAlign = 'center';
203
+ ctx.fillText(`${r.name} ends`, x, PAD.top + plotH - 14 - 11 * (lane++ % 3));
204
+ }
205
+ }
206
+ }
207
+ ctx.font = '11px ui-monospace, SFMono-Regular, Menlo, monospace';
208
+
209
+ // readout: the pointer's price, or the sums near the mid
210
+ ctx.textAlign = 'left'; ctx.fillStyle = INK.bright;
211
+ if (hover && hover.x >= PAD.left && hover.x <= w - PAD.right) {
212
+ const p = pmin + ((hover.x - PAD.left) / plotW) * (pmax - pmin);
213
+ const i = Math.max(0, Math.min(ser.prices.length - 1, Math.round((p - ser.prices[0]) / ser.step)));
214
+ const bidSide = ser.prices[i] <= ser.mid;
215
+ const cum = bidSide ? ser.bid[i] : ser.ask[i];
216
+ const per = ser.exchanges.map((e) => [e.name, bidSide ? e.bids[i] : e.asks?.[i]]).filter(([, v]) => v != null).map(([nm, v]) => `${nm} ${btc(v)}`).join(' · ');
217
+ ctx.strokeStyle = INK.dash; ctx.setLineDash([3, 3]);
218
+ const x = Math.round(X(ser.prices[i])) + 0.5;
219
+ line(x, PAD.top, x, PAD.top + plotH);
220
+ ctx.setLineDash([]);
221
+ const part = bidSide ? ser.bidPart[i] : ser.askPart[i];
222
+ ctx.fillText(`$${money(ser.prices[i])} · ${bidSide ? 'bid at or above' : 'asked at or below'}: ${part ? 'at least ' : ''}${btc(cum)} BTC${ser.change ? ` · change here ${ser.change[i] > 0 ? '+' : ''}${btc(ser.change[i])} BTC` : ''}${per ? ` (${per})` : ''}`, PAD.left + 2, 12);
223
+ } else {
224
+ const s1 = depthSums(ser, 0.01), s5 = depthSums(ser, 0.05);
225
+ const al = (s) => (s.atLeast ? '≥ ' : '');
226
+ ctx.fillText(compact
227
+ ? `mid $${money(ser.mid)} · 1%: ${al(s1)}${btc(s1.bids)} / ${btc(s1.asks)} BTC`
228
+ : `mid $${money(ser.mid)} · within 1%: ${al(s1)}bids ${btc(s1.bids)} / asks ${btc(s1.asks)} BTC · within 5%: ${al(s5)}bids ${btc(s5.bids)} / asks ${btc(s5.asks)} BTC (dotted: a book has ended, so at least)`,
229
+ PAD.left + 2, compact ? 8 : 12);
230
+ }
231
+ return { pmin, pmax, vmax, F };
232
+ }
233
+
234
+ export function depthBarHtml(ago = D.ago, zoom = D.zoom) {
235
+ const b = (attr, v, label, on) => `<button type="button" class="mkbtn${on ? ' on' : ''}" ${attr}="${v}">${label}</button>`;
236
+ return `<div class="mkgrp"><span class="mklab">change bars for the last</span>${AGOS.map(([s, l]) => b('data-ago', s, l, s === ago)).join('')}</div>
237
+ <div class="mkgrp"><span class="mklab">price window</span>${ZOOMS.map(([z, l]) => b('data-zoom', z, l, z === zoom)).join('')}</div>`;
238
+ }
239
+
240
+ // what the empty chart says while the feed is off: the switch, or the server's hard off
241
+ const offText = (d) => (d?.enabled === false ? (d.polling === false ? 'market polling is off — Display settings → Markets & Price → Enable market polling' : 'market data is off on this server') : null);
242
+
243
+ export function depthNote(d, fmt) {
244
+ if (!d) return 'asking the exchanges for their order books…';
245
+ if (d.enabled === false) return d.note ?? 'market data is off on this monitor';
246
+ if (d.warming) return 'reading the order books for the first time…';
247
+ const agoL = AGOS.find(([s]) => s === d.ago)?.[1] ?? `${d.ago} s`;
248
+ const parts = [`books read at ${clock(d.at)} UTC, every ${Math.round((d.bookMs ?? DEPTH_MS) / 1000)} s while this tab is open`];
249
+ parts.push(d.thenAt ? `the dashed line and the change bars compare with ${clock(d.thenAt)} UTC (${agoL} before)` : `no snapshot ${agoL} old yet -- the books have been read since ${clock(d.firstAt)} UTC, so the change bars wait`);
250
+ const bad = (d.exchanges ?? []).filter((e) => e.error);
251
+ if (bad.length) parts.push(`not read: ${bad.map((e) => `${fmt.esc(e.name)} (${fmt.esc(e.error)})`).join(', ')}`);
252
+ parts.push('OKX quotes USDT; it is in the total');
253
+ return parts.join(' · ');
254
+ }
255
+
256
+ function draw() {
257
+ const canvas = document.getElementById('mkDepth');
258
+ if (!canvas) return;
259
+ drawDepth(canvas, depthSeries(D.data), { zoom: D.zoom, hover: D.hover, empty: offText(D.data) });
260
+ }
261
+
262
+ /**
263
+ * ONE FETCH, HOWEVER MANY VIEWS. The Markets tab and the Kiosk both want the books; this is the
264
+ * shared poll, so opening both does not double the traffic to five exchanges. The server's route
265
+ * calls markets.touch(), so a Kiosk polling on its own keeps the collector out of its idle park --
266
+ * a wall display needs no special handling.
267
+ */
268
+ function ensureDepth(h) {
269
+ const now = Date.now();
270
+ const key = String(D.ago);
271
+ if (!D.busy && (D.key !== key || now - D.at >= (D.data && !D.data.warming ? DEPTH_MS : 4000))) {
272
+ D.busy = true; D.at = now; D.key = key;
273
+ h.api(`/api/markets/depth?ago=${D.ago}`)
274
+ .then((d) => { if (D.key === key) { D.data = d; D.error = null; } })
275
+ .catch((err) => { D.error = err.message; })
276
+ .finally(() => { D.busy = false; h.render(); });
277
+ }
278
+ }
279
+
280
+ /**
281
+ * The chart into a canvas of the caller's choosing, compact and with no toolbar -- for the Kiosk.
282
+ * It deliberately does NOT touch D.hover or D.zoom: those belong to the Markets tab's chart, and
283
+ * sharing them would make a pointer on one view redraw the other.
284
+ */
285
+ export function renderDepthInto(canvasId, h, { zoom = 0.025, compact = true } = {}) {
286
+ ensureDepth(h);
287
+ const canvas = document.getElementById(canvasId);
288
+ if (!canvas) return null;
289
+ return drawDepth(canvas, depthSeries(D.data), { zoom, compact, empty: offText(D.data) });
290
+ }
291
+
292
+ export function renderDepth(h) {
293
+ ensureDepth(h);
294
+ const bar = document.getElementById('mkDepthBar'), canvas = document.getElementById('mkDepth'), note = document.getElementById('mkDepthNote');
295
+ if (!bar || !canvas) return;
296
+ const html = depthBarHtml();
297
+ if (bar.__html !== html) { bar.innerHTML = html; bar.__html = html; }
298
+ if (!D.bound) {
299
+ D.bound = true;
300
+ bar.addEventListener('click', (e) => {
301
+ const btn = e.target.closest('button');
302
+ if (!btn) return;
303
+ if (btn.dataset.ago) D.ago = Number(btn.dataset.ago);
304
+ if (btn.dataset.zoom) D.zoom = Number(btn.dataset.zoom);
305
+ h.render();
306
+ });
307
+ canvas.addEventListener('pointermove', (e) => { const r = canvas.getBoundingClientRect(); D.hover = { x: e.clientX - r.left, y: e.clientY - r.top }; draw(); });
308
+ canvas.addEventListener('pointerleave', () => { D.hover = null; draw(); });
309
+ }
310
+ draw();
311
+ if (note) {
312
+ const t = D.error ? h.fmt.esc(D.error) : depthNote(D.data, h.fmt);
313
+ if (note.__html !== t) { note.innerHTML = t; note.__html = t; }
314
+ }
315
+ }