blockyard 0.0.1 → 0.0.9

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 (107) hide show
  1. package/CHANGELOG.md +679 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +4 -0
  4. package/README.md +172 -4
  5. package/SECURITY.md +38 -0
  6. package/bin/blockyard.js +40 -0
  7. package/config/pool-map.json +2620 -0
  8. package/docs/API.md +1575 -0
  9. package/docs/ARCHITECTURE.md +1307 -0
  10. package/docs/AUTO-UPDATE.md +269 -0
  11. package/docs/CONFIGURATION.md +840 -0
  12. package/docs/DEFECTS.md +813 -0
  13. package/docs/EFFECTS-AGENTS.md +448 -0
  14. package/docs/GETTING-STARTED.md +202 -0
  15. package/docs/INSTALL.md +490 -0
  16. package/docs/MEASUREMENTS.md +1254 -0
  17. package/docs/PRIVATE-LEADERBOARD.md +230 -0
  18. package/docs/RULES.md +681 -0
  19. package/docs/SECURITY-AUDIT-2026-09-14.md +177 -0
  20. package/docs/SECURITY-AUDIT.md +258 -0
  21. package/docs/SECURITY.md +195 -0
  22. package/docs/STATE-2026-09-09.md +200 -0
  23. package/docs/TROUBLESHOOTING.md +298 -0
  24. package/docs/USER-GUIDE.md +1022 -0
  25. package/package.json +53 -5
  26. package/public/404.html +9 -0
  27. package/public/css/app.css +1785 -0
  28. package/public/index.html +893 -0
  29. package/public/js/about.js +112 -0
  30. package/public/js/agents.js +964 -0
  31. package/public/js/app.js +1312 -0
  32. package/public/js/arkanoid.js +806 -0
  33. package/public/js/blockanoid.js +347 -0
  34. package/public/js/blockout.js +347 -0
  35. package/public/js/blockpack.js +428 -0
  36. package/public/js/blockscene3d.js +2678 -0
  37. package/public/js/breakout.js +224 -0
  38. package/public/js/charts.js +635 -0
  39. package/public/js/depthchart.js +311 -0
  40. package/public/js/details3d.js +2957 -0
  41. package/public/js/explorer.js +405 -0
  42. package/public/js/feepalette.js +149 -0
  43. package/public/js/fmt.js +162 -0
  44. package/public/js/goggles.js +886 -0
  45. package/public/js/kiosk.js +41 -0
  46. package/public/js/login.js +83 -0
  47. package/public/js/markets.js +357 -0
  48. package/public/js/mining.js +1138 -0
  49. package/public/js/panels.js +966 -0
  50. package/public/js/pricechart.js +188 -0
  51. package/public/js/settings.js +1014 -0
  52. package/public/js/tetris.js +226 -0
  53. package/public/js/tetrust.js +356 -0
  54. package/public/js/tetsound.js +175 -0
  55. package/public/login.html +33 -0
  56. package/scripts/blockfile-measure.js +156 -0
  57. package/scripts/browser-check.mjs +286 -0
  58. package/scripts/check.js +173 -0
  59. package/scripts/decode-check.js +81 -0
  60. package/scripts/doc-counts.js +109 -0
  61. package/scripts/donate-qr.py +20 -0
  62. package/scripts/fake-node.js +534 -0
  63. package/scripts/index-bench.js +216 -0
  64. package/scripts/index-benchmark.js +117 -0
  65. package/scripts/index-build.js +40 -0
  66. package/scripts/live-render-check.mjs +89 -0
  67. package/scripts/manage-users.js +132 -0
  68. package/scripts/motion-check.mjs +138 -0
  69. package/scripts/pool-map.js +157 -0
  70. package/scripts/setup.js +410 -0
  71. package/scripts/shots.mjs +272 -0
  72. package/scripts/smoke.sh +327 -0
  73. package/scripts/ui.js +174 -0
  74. package/server/auth/sessions.js +221 -0
  75. package/server/auth/users.js +243 -0
  76. package/server/chain/blockfile.js +234 -0
  77. package/server/chain/index/build.js +193 -0
  78. package/server/chain/index/heights.js +36 -0
  79. package/server/chain/index/live.js +276 -0
  80. package/server/chain/index/rows.js +145 -0
  81. package/server/chain/index/store.js +154 -0
  82. package/server/chain/index/worker.js +109 -0
  83. package/server/chain/tx.js +310 -0
  84. package/server/collect/gbt.js +229 -0
  85. package/server/collect/logparse.js +765 -0
  86. package/server/collect/logtail.js +189 -0
  87. package/server/collect/markets.js +333 -0
  88. package/server/collect/mining.js +333 -0
  89. package/server/collect/monitor.js +2516 -0
  90. package/server/collect/nextblock.js +275 -0
  91. package/server/collect/sync.js +386 -0
  92. package/server/config.js +620 -0
  93. package/server/http/api.js +1275 -0
  94. package/server/http/explorer.js +418 -0
  95. package/server/http/server.js +412 -0
  96. package/server/http/sse.js +176 -0
  97. package/server/http/static.js +212 -0
  98. package/server/main.js +628 -0
  99. package/server/netinfo.js +253 -0
  100. package/server/rpc/allowlist.js +130 -0
  101. package/server/rpc/client.js +414 -0
  102. package/server/store/audit.js +148 -0
  103. package/server/store/history.js +220 -0
  104. package/server/store/ledger.js +290 -0
  105. package/server/store/ring.js +173 -0
  106. package/server/util/fmt.js +29 -0
  107. package/systemd/blockyard.service +100 -0
@@ -0,0 +1,893 @@
1
+ <!doctype html>
2
+ <!-- data-blockyard-build is rewritten per response by server/http/static.js, so the page
3
+ can compare the build it was served against what /api/build says is current and
4
+ say "this tab is stale" out loud. `%BLOCKYARD_BUILD%` is the placeholder. -->
5
+ <html lang="en" data-blockyard-build="%BLOCKYARD_BUILD%">
6
+ <head>
7
+ <meta charset="utf-8">
8
+ <meta name="viewport" content="width=device-width, initial-scale=1">
9
+ <meta name="referrer" content="no-referrer">
10
+ <title>BlockYard - Bitcoin node monitor</title>
11
+ <link rel="stylesheet" href="/css/app.css">
12
+ <link rel="icon" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22g%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23ffb85c%22%2F%3E%3Cstop%20offset%3D%22.55%22%20stop-color%3D%22%23f7931a%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23a8640d%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%2232%22%20height%3D%2232%22%20rx%3D%227.5%22%20fill%3D%22url%28%23g%29%22%2F%3E%3Cg%20fill%3D%22%2316110a%22%20opacity%3D%22.14%22%3E%3Crect%20x%3D%224%22%20y%3D%2224.6%22%20width%3D%228%22%20height%3D%223.6%22%20rx%3D%221%22%2F%3E%3Crect%20x%3D%2213%22%20y%3D%2224.6%22%20width%3D%226%22%20height%3D%223.6%22%20rx%3D%221%22%2F%3E%3Crect%20x%3D%2220%22%20y%3D%2224.6%22%20width%3D%228%22%20height%3D%223.6%22%20rx%3D%221%22%2F%3E%3C%2Fg%3E%3Cpath%20fill%3D%22%231a1206%22%20fill-rule%3D%22evenodd%22%20d%3D%22M5%208h6.6a4.2%204.2%200%200%201%202.35%207.66A4.4%204.4%200%200%201%2012%2024H5Zm3.3%202.9v3.5h3.1a1.75%201.75%200%200%200%200-3.5Zm0%206.4v3.8h3.4a1.9%201.9%200%200%200%200-3.8Z%22%2F%3E%3Cpath%20fill%3D%22%231a1206%22%20d%3D%22M16.8%208h3.3l2.75%205.3L25.6%208h3.3l-4.85%208.8V24h-3.3v-7.2Z%22%2F%3E%3C%2Fsvg%3E">
13
+ </head>
14
+ <body>
15
+ <div class="app">
16
+ <header class="top">
17
+ <!-- The monogram IS the About link (operator: "Make clicking the BlockYard icon the About
18
+ page then"). A <button data-page> rather than a bare click handler, so it satisfies the
19
+ same page contract every nav tab does: test/web-contract.test.js holds every
20
+ `<button data-page>` to a matching <section> and a render case. -->
21
+ <button type="button" data-page="about" class="brand" id="brandAbout" title="About BlockYard" aria-label="About BlockYard"><svg class="mark" viewBox="0 0 32 32" role="img" aria-label="BlockYard">
22
+ <defs><linearGradient id="byTile" x1="0" y1="0" x2="1" y2="1">
23
+ <stop offset="0" stop-color="#ffb85c"/><stop offset=".55" stop-color="#f7931a"/><stop offset="1" stop-color="#a8640d"/>
24
+ </linearGradient></defs>
25
+ <rect x=".5" y=".5" width="31" height="31" rx="7.5" fill="url(#byTile)"/>
26
+ <g fill="#16110a" opacity=".14"><rect x="4" y="24.6" width="8" height="3.6" rx="1"/><rect x="13" y="24.6" width="6" height="3.6" rx="1"/><rect x="20" y="24.6" width="8" height="3.6" rx="1"/></g>
27
+ <path fill="#1a1206" fill-rule="evenodd" d="M5 8h6.6a4.2 4.2 0 0 1 2.35 7.66A4.4 4.4 0 0 1 12 24H5Zm3.3 2.9v3.5h3.1a1.75 1.75 0 0 0 0-3.5Zm0 6.4v3.8h3.4a1.9 1.9 0 0 0 0-3.8Z"/>
28
+ <path fill="#1a1206" d="M16.8 8h3.3l2.75 5.3L25.6 8h3.3l-4.85 8.8V24h-3.3v-7.2Z"/>
29
+ </svg>
30
+ <span class="wordmark">Block<b>Yard</b></span> <small id="ver"></small></button>
31
+ <nav class="pages" id="nav">
32
+ <button data-page="overview" class="on">Overview</button>
33
+ <button data-page="space">Block space</button>
34
+ <button data-page="chain">Chain &amp; Sync</button>
35
+ <button data-page="mempool">Mempool</button>
36
+ <button data-page="explorer">Explorer</button>
37
+ <button data-page="markets">Markets</button>
38
+ <button data-page="kiosk">Kiosk</button>
39
+ <button data-page="peers">Peers</button>
40
+ <button data-page="network">Network</button>
41
+ <button data-page="mining">Mining</button>
42
+ <button data-page="logs">Events</button>
43
+ <button data-page="node">Node &amp; RPC</button>
44
+ <button data-page="admin" id="navAdmin" hidden>Admin</button>
45
+ </nav>
46
+ <!-- DIVERSIONS (operator, 2026-09-12: "Move Tetrust and Blockout to the very end of the menu
47
+ options under a pop-down menu called 'Diversions'"). The toggle deliberately carries no
48
+ data-page, because a nav button with one is required to have a page section behind it.
49
+ OUTSIDE <nav>, and that is the whole point (operator, Safari on macOS: "still doesn't work
50
+ on safari. not clickable"). It used to sit inside `nav.pages`, which is a horizontal
51
+ scroll container, held at the right edge with `position: sticky; right: 0`. WebKit paints
52
+ a sticky element in its stuck position but hit-tests it where layout originally put it, so
53
+ the button was plainly visible and completely dead to the pointer -- in that engine only,
54
+ which is why every check here passed. Out here it is an ordinary flex item in the header
55
+ row: no sticky, no scroll container, nothing to go wrong. Being outside <nav> also means
56
+ the nav's delegated click handler no longer reaches the items, so app.js routes them from
57
+ the panel's own handler. -->
58
+ <div class="navmenu" id="navDiv">
59
+ <button type="button" class="navmenu-btn" id="navDivBtn" aria-haspopup="true" aria-expanded="false">Diversions <span class="navcaret">&#9662;</span></button>
60
+ </div>
61
+ <span class="spacer"></span>
62
+ <div class="meta">
63
+ <span id="nodePick"></span>
64
+ <span title="Server-sent events link">stream <b id="sseState" class="warn">connecting</b></span>
65
+ <span title="Node RPC round trip">rpc <b id="rpcLat">–</b></span>
66
+ <span title="Monitor uptime">up <b id="appUp">–</b></span>
67
+ <!-- Shown only when the server reports accounts=false. The posture has to be
68
+ visible in the page, not inferred from a sign-out button that is missing. -->
69
+ <span id="accessPill" class="hidden"><b class="badge muted">open access</b></span>
70
+ <!-- Only shown when the loaded build differs from the served one. Its whole
71
+ purpose is the case where nothing else on screen has changed. -->
72
+ <span id="buildNote" class="hidden"><b class="badge bad">stale build</b></span>
73
+ </div>
74
+ <div class="userchip">
75
+ <span class="tiny faint" id="whoami"></span>
76
+ <button class="btn gear" id="btnSettings" title="Display settings" aria-label="Display settings" aria-expanded="false">
77
+ <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"><path d="M 13.94 2 c 0.50 0 0.92 0.37 0.99 0.87 l 0.26 1.85 c 0.50 0.18 0.97 0.41 1.40 0.69 l 1.73 -0.72 c 0.45 -0.19 0.98 -0.02 1.25 0.43 l 1.44 2.50 c 0.26 0.45 0.16 1.02 -0.24 1.28 l -1.47 1.16 c 0.04 0.28 0.06 0.56 0.06 0.84 c 0 0.28 -0.02 0.56 -0.06 0.84 l 1.47 1.16 c 0.40 0.26 0.50 0.83 0.24 1.28 l -1.44 2.50 c -0.27 0.45 -0.80 0.62 -1.25 0.43 l -1.73 -0.72 c -0.43 0.28 -0.90 0.51 -1.40 0.69 l -0.26 1.85 c -0.07 0.50 -0.49 0.87 -0.99 0.87 h -2.88 c -0.50 0 -0.92 -0.37 -0.99 -0.87 l -0.26 -1.85 c -0.50 -0.18 -0.97 -0.41 -1.40 -0.69 l -1.73 0.72 c -0.45 0.19 -0.98 0.02 -1.25 -0.43 l -1.44 -2.50 c -0.26 -0.45 -0.16 -1.02 0.24 -1.28 l 1.47 -1.16 c -0.04 -0.28 -0.06 -0.56 -0.06 -0.84 c 0 -0.28 0.02 -0.56 0.06 -0.84 l -1.47 -1.16 c -0.40 -0.26 -0.50 -0.83 -0.24 -1.28 l 1.44 -2.50 c 0.27 -0.45 0.80 -0.62 1.25 -0.43 l 1.73 0.72 c 0.43 -0.28 0.90 -0.51 1.40 -0.69 l 0.26 -1.85 c 0.07 -0.50 0.49 -0.87 0.99 -0.87 z M 12 8.40 a 3.60 3.60 0 1 0 0 7.20 a 3.60 3.60 0 0 0 0 -7.20 z"/></svg>
78
+ </button>
79
+ <button class="btn" id="btnPause" title="Freeze live updates (hold Shift to also stop the event feed)">pause</button>
80
+ <button class="btn" id="btnLogout">sign out</button>
81
+ </div>
82
+ </header>
83
+
84
+ <!-- THE DIVERSIONS PANEL, a direct child of <body> (operator, Safari on macOS: "rendering on
85
+ safari is still broken. It's only showing half the drop-down contents").
86
+ It used to sit inside `header.top`, which is `overflow: hidden` and `height: 46px`. The panel
87
+ opens ~32px down and stands ~88px tall, so everything past the header's 46px was cut -- about
88
+ half of it, in WebKit. `position: fixed` is supposed to escape an ancestor's overflow, and in
89
+ Chrome and Firefox it did, which is why this looked fine everywhere it was tested.
90
+ The settings dialog (.cfgwrap) is the proof of the right shape: also fixed, also an overlay,
91
+ and it renders correctly in Safari. It sits out here at the top level of .app rather than
92
+ inside the header, and so does this panel now -- .app is a plain grid with no overflow, no
93
+ transform and no filter, so nothing between this element and the viewport can clip it or
94
+ become its containing block. (It is NOT a child of <body>: the page is wrapped in .app. The
95
+ invariant that matters is "not inside the clipped header", which is what the test pins.)
96
+ Its button stays in the header; only the panel moved, so app.js positions it from that
97
+ button's rect and routes the games from here. -->
98
+ <div class="navmenu-pop hidden" id="navDivPop" role="menu">
99
+ <button data-page="tetrust" role="menuitem" title="Tetrust: trust, but verify">Tetrust</button>
100
+ <button data-page="blockout" role="menuitem" title="Blockout: break the wall">Blockout</button>
101
+ <button data-page="blockanoid" role="menuitem" title="Blockanoid: Vaus, capsules and the minions">Blockanoid</button>
102
+ </div>
103
+
104
+ <!-- Display settings (settings.js). Hidden until the gear is pressed; the contents are
105
+ rendered from settings.js PANEL, so a value and its control cannot drift apart. -->
106
+ <div class="cfgwrap hidden" id="settingsWrap">
107
+ <div class="cfgscrim" id="settingsScrim"></div>
108
+ <section class="cfg" id="settingsPanel" role="dialog" aria-modal="true" aria-labelledby="cfgTitle">
109
+ <h2 id="cfgTitle">Display settings <span class="sp"></span>
110
+ <button class="btn" id="cfgReset" title="Put every setting back to the shipped default">reset</button>
111
+ <button class="btn" id="cfgClose" aria-label="Close settings">close</button>
112
+ </h2>
113
+ <p class="cfgnote">Kept in this browser only, and applied as soon as you change them. They
114
+ change what is drawn, not what is measured: every number on the page is the same either way.</p>
115
+ <div id="cfgBody"></div>
116
+ </section>
117
+ </div>
118
+
119
+ <main>
120
+ <div id="offline" class="offline-bar hidden"></div>
121
+
122
+ <!-- ======================================================== OVERVIEW -->
123
+ <section class="page on" data-page="overview">
124
+ <div class="grid">
125
+ <div data-sync-hero="overview"></div>
126
+
127
+ <!-- The markets summary, between the sync strip and Block flow (operator, 2026-09-13:
128
+ "We really need to squeeze this line into the top of the Overview"). Hidden unless
129
+ markets.overviewSummary is on: the figures need the exchange feed, and Overview is
130
+ the landing page -- see the note on that setting. Same class as the Markets page's
131
+ own line, and filled by the same summaryHtml(), so the two cannot drift. -->
132
+ <div class="mksum ovmksum" id="ovMkSummary" hidden></div>
133
+
134
+ <!-- Two independent stacks (operator, 2026-09-11: "Still too much empty
135
+ space here. Need to compress the mempool and fees view at the very
136
+ least to sit side-by-side"). A shared grid row coupled the columns,
137
+ so whichever side was taller padded the other; here each column
138
+ stacks on its own, and the last card of each takes the height that
139
+ is left and scrolls inside it. -->
140
+ <div class="ovcols">
141
+ <div class="ovcol">
142
+ <div class="card w8">
143
+ <h3>Block flow <span class="sp"></span><span class="src">being assembled → <b>the current block</b> → history · <a href="#mining">packages &amp; goggles</a></span></h3>
144
+ <div id="ovTrain" class="flowwrap"></div>
145
+ </div>
146
+
147
+ <div class="ovpair">
148
+ <div class="card w5" id="ovMempoolCard">
149
+ <h3>Mempool <span class="sp"></span><span class="src">getmempoolinfo · vs blocks mined</span></h3>
150
+ <div class="metric"><div class="l">transactions</div><div><span class="v" id="ovMpCount">–</span><span class="u">tx</span></div>
151
+ <div class="s" id="ovMpBytes">–</div></div>
152
+ <canvas class="chart xs" id="ovMpChart"></canvas>
153
+ <div class="s faint" id="ovMpVsBlock">–</div>
154
+ <div class="kv" id="ovMpVsBlockKv"></div>
155
+ </div>
156
+
157
+ <div class="card w3 compact" id="ovFeesCard">
158
+ <h3>Fees <span class="sp"></span><span class="src">estimatesmartfee</span></h3>
159
+ <div class="kv" id="ovFees"><dt>–</dt></div>
160
+ <canvas class="chart xs" id="ovFeeChart"></canvas>
161
+ </div>
162
+ </div>
163
+
164
+ <div class="card w8" id="ovBlocksCard">
165
+ <h3>Last blocks <span class="sp"></span><span class="src">getblockstats + [block] stored</span></h3>
166
+ <div class="scroll md">
167
+ <table class="t" id="ovBlocks">
168
+ <thead><tr><th>height</th><th>age</th><th>gap</th><th class="r">txs</th><th class="r">size</th><th class="r">vsize</th><th class="r">fees (btc)</th><th class="r">min fee rate</th><th>from peer</th></tr></thead>
169
+ <tbody></tbody>
170
+ </table>
171
+ </div>
172
+ </div>
173
+ </div>
174
+
175
+ <div class="ovcol">
176
+ <div class="card w8">
177
+ <h3>Block space <span class="sp"></span><span class="src">area = vbytes, colour = feerate paid</span></h3>
178
+ <div class="treemapwrap sq">
179
+ <canvas class="treemap" id="ovGnTreemap"></canvas><div class="viewer-ctl"><div class="refresh-in" data-refresh></div><button type="button" class="refresh-now" data-refresh-now disabled>refresh now</button></div>
180
+ <div class="goggles-tip hidden"></div>
181
+ </div>
182
+ <div class="note tiny faint" id="ovGnTreemapNote"></div>
183
+ </div>
184
+
185
+ <div class="card w4" id="ovEventsCard">
186
+ <h3>Monitor events <span class="sp"></span><span class="src">what this monitor observed; not node log lines</span></h3>
187
+ <div class="feed" id="ovFeed"></div>
188
+ </div>
189
+ </div>
190
+ </div>
191
+
192
+ <div class="card w12">
193
+ <h3>Mined by <span class="sp"></span><span class="src">coinbase text + curated labels</span></h3>
194
+ <div id="ovMiningPools"></div>
195
+ </div>
196
+
197
+ <div class="card w12" id="ovCaveats"></div>
198
+ </div>
199
+ </section>
200
+
201
+ <!-- ===================================================== BLOCK SPACE -->
202
+ <!-- The viewer at full size, with the block being built and the chain tip
203
+ beside it (operator, 2026-09-11: "blockspace viewer really deserves its
204
+ own tab so it can run in a large window. Try to incorporate current
205
+ block building, and tip information into the one panel as well"). -->
206
+ <section class="page" data-page="space">
207
+ <div class="card w12" id="spaceCard">
208
+ <h3>Block space <span class="sp"></span><span class="src">the next block's worth of the mempool, richest first · area = vbytes, colour = feerate paid</span></h3>
209
+ <div class="spacelayout">
210
+ <div class="treemapwrap space">
211
+ <canvas class="treemap" id="spTreemap"></canvas><div class="viewer-ctl"><div class="refresh-in" data-refresh></div><button type="button" class="refresh-now" data-refresh-now disabled>refresh now</button></div>
212
+ <div class="goggles-tip hidden"></div>
213
+ </div>
214
+ <aside class="spacehud">
215
+ <div class="hud" id="spNext"></div>
216
+ <div class="hud" id="spTip"></div>
217
+ <div class="hud" id="spLegend"></div>
218
+ </aside>
219
+ </div>
220
+ <div class="note tiny faint" id="spNote"></div>
221
+ </div>
222
+ </section>
223
+
224
+ <!-- ==================================================== CHAIN & SYNC -->
225
+ <section class="page" data-page="chain">
226
+ <div class="grid chgrid">
227
+ <div data-sync-hero="chain"></div>
228
+ <!-- THREE ACROSS, SHORT (operator, 2026-09-11: "surely we can fit more info on here").
229
+ Two columns of tall charts made the page 1,518 px at 1483 x 1000 and 1,762 at
230
+ 1280 x 860; the tip chart is a flat line once synced, and Chain state was a 12-row
231
+ single column. Charts are three to a row at 110 px (tip progression last), the
232
+ state and UTXO/difficulty cards three across with two-column lists. -->
233
+
234
+ <div class="card w4">
235
+ <h3>Block interval <span class="sp"></span><span class="src">inter-arrival, last 24h</span></h3>
236
+ <canvas class="chart" id="chGapChart"></canvas>
237
+ <div class="note tiny faint" id="chGapNote"></div>
238
+ </div>
239
+
240
+ <div class="card w4">
241
+ <h3>Block size <span class="sp"></span><span class="src">bytes, sum of transaction sizes</span></h3>
242
+ <canvas class="chart" id="chSizeChart"></canvas>
243
+ <div class="note tiny faint" id="chSizeNote"></div>
244
+ </div>
245
+
246
+ <div class="card w4">
247
+ <h3>Fees per block <span class="sp"></span><span class="src">satsoshis</span></h3>
248
+ <canvas class="chart" id="chFeeChart"></canvas>
249
+ </div>
250
+
251
+ <div class="card w4">
252
+ <h3>Transactions per block <span class="sp"></span><span class="src">count</span></h3>
253
+ <canvas class="chart" id="chTxChart"></canvas>
254
+ </div>
255
+
256
+ <div class="card w4">
257
+ <h3>Transaction rate <span class="sp"></span><span class="src">getchaintxstats txrate</span></h3>
258
+ <canvas class="chart" id="chTxRateChart"></canvas>
259
+ <div class="kv two mt-6" id="chTxStats"></div>
260
+ </div>
261
+
262
+ <div class="card w4">
263
+ <h3>Tip progression <span class="sp"></span><span class="src">blocks vs headers</span></h3>
264
+ <canvas class="chart" id="chTipChart"></canvas>
265
+ <div class="note tiny faint" id="chTipNote"></div>
266
+ </div>
267
+
268
+ <div class="card w4">
269
+ <h3>Chain state <span class="sp"></span><span class="src">getblockchaininfo</span></h3>
270
+ <dl class="kv two" id="chState"></dl>
271
+ </div>
272
+
273
+ <div class="card w4">
274
+ <h3>UTXO set <span class="sp"></span><span class="src">gettxoutsetinfo</span></h3>
275
+ <dl class="kv" id="chUtxo"></dl>
276
+ <canvas class="chart short" id="chUtxoChart"></canvas>
277
+ </div>
278
+
279
+ <div class="card w4">
280
+ <h3>Difficulty &amp; work <span class="sp"></span><span class="src">difficulty</span></h3>
281
+ <dl class="kv" id="chDiff"></dl>
282
+ <canvas class="chart short" id="chDiffChart"></canvas>
283
+ </div>
284
+
285
+ <div class="card w6">
286
+ <h3>Block drill-down <span class="sp"></span><span class="src">getblock verbosity=1 + getblockstats</span></h3>
287
+ <div class="rowform">
288
+ <input class="f grow minw-180" id="bdQuery" placeholder="height or block hash (blank = current tip)" autocomplete="off">
289
+ <button class="btn primary" id="bdRun">inspect</button>
290
+ </div>
291
+ <div class="note tiny faint" id="bdNote">Header, statistics and the first page of txids. Not the block's transactions: this node returns 11 MB of hex for one verbose block, and its verbose form still omits Core's fee fields.</div>
292
+ <dl class="kv" id="bdHeader"></dl>
293
+ <div class="txid-list" id="bdTxids"></div>
294
+ <div class="drill" id="bdTx"></div>
295
+ </div>
296
+
297
+ <div class="card w6">
298
+ <h3>Indexes <span class="sp"></span><span class="src">getindexinfo</span></h3>
299
+ <table class="t" id="chIndexes"><thead><tr><th>index</th><th class="r">height</th><th>synced</th></tr></thead><tbody></tbody></table>
300
+ <div class="hr"></div>
301
+ <h3>Chain tips <span class="sp"></span><span class="src">getchaintips</span></h3>
302
+ <table class="t" id="chTips"><thead><tr><th>height</th><th class="r">branchlen</th><th>status</th></tr></thead><tbody></tbody></table>
303
+ </div>
304
+ </div>
305
+ </section>
306
+
307
+ <!-- ========================================================== ABOUT -->
308
+ <!-- Reached from the monogram, not from a nav tab. The galaxy is the point here, not a
309
+ backdrop: this is the one page with no data for it to obscure. -->
310
+ <section class="page" data-page="about">
311
+ <div class="aboutwrap">
312
+ <canvas class="absky" id="abSky"></canvas>
313
+ <div class="aboutinner">
314
+ <div class="abhead">
315
+ <svg class="ablogo" viewBox="0 0 32 32" role="img" aria-label="BlockYard">
316
+ <defs><linearGradient id="abTile" x1="0" y1="0" x2="1" y2="1">
317
+ <stop offset="0" stop-color="#ffb85c"/><stop offset=".55" stop-color="#f7931a"/><stop offset="1" stop-color="#a8640d"/>
318
+ </linearGradient></defs>
319
+ <rect x=".5" y=".5" width="31" height="31" rx="7.5" fill="url(#abTile)"/>
320
+ <g fill="#16110a" opacity=".14"><rect x="4" y="24.6" width="8" height="3.6" rx="1"/><rect x="13" y="24.6" width="6" height="3.6" rx="1"/><rect x="20" y="24.6" width="8" height="3.6" rx="1"/></g>
321
+ <path fill="#1a1206" fill-rule="evenodd" d="M5 8h6.6a4.2 4.2 0 0 1 2.35 7.66A4.4 4.4 0 0 1 12 24H5Zm3.3 2.9v3.5h3.1a1.75 1.75 0 0 0 0-3.5Zm0 6.4v3.8h3.4a1.9 1.9 0 0 0 0-3.8Z"/>
322
+ <path fill="#1a1206" d="M16.8 8h3.3l2.75 5.3L25.6 8h3.3l-4.85 8.8V24h-3.3v-7.2Z"/>
323
+ </svg>
324
+ <div>
325
+ <h1>Block<b>Yard</b></h1>
326
+ <p>A self-hosted monitor, block explorer, markets view and 3D block-space viewer for a Bitcoin node. No dependencies, no telemetry, read-only toward your node.</p>
327
+ </div>
328
+ </div>
329
+
330
+ <div class="abcols">
331
+ <section class="abcard"><h3>This monitor</h3><div id="abApp"></div></section>
332
+ <section class="abcard"><h3>Host</h3><div id="abHost"></div></section>
333
+ <section class="abcard"><h3>Bitcoin node</h3><div id="abNode"></div></section>
334
+ </div>
335
+
336
+ <a class="abgh" href="https://github.com/BobClawblaw/blockyard" target="_blank" rel="noopener noreferrer">
337
+ <!-- The GitHub mark, GitHub's trademark, used to link to the project on GitHub. -->
338
+ <svg viewBox="0 0 16 16" width="20" height="20" aria-hidden="true" focusable="false" fill="currentColor"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg>
339
+ <span>github.com/BobClawblaw/blockyard</span>
340
+ </a>
341
+
342
+ <!-- The donation address: one click copies it (operator, 2026-09-14). A button, not a link:
343
+ there is nowhere to go, and the address is the whole point. -->
344
+ <button type="button" class="abdon" id="abDonate" data-copy="bc1q249cv27lc2q7y0x53vkczgfvvgsjzhwxwv42gc" title="click to copy the address">
345
+ <!-- The QR is the address itself, generated once (scripts/donate-qr.py) and checked to decode back to it. -->
346
+ <svg class="abqr" viewBox="0 0 41 41" width="132" height="132" role="img" aria-label="QR code of the donation address" shape-rendering="crispEdges"><rect width="41" height="41" fill="#fff"/><path fill="#000" d="M6 6h1v1h-1zM7 6h1v1h-1zM8 6h1v1h-1zM9 6h1v1h-1zM10 6h1v1h-1zM11 6h1v1h-1zM12 6h1v1h-1zM16 6h1v1h-1zM18 6h1v1h-1zM20 6h1v1h-1zM22 6h1v1h-1zM23 6h1v1h-1zM24 6h1v1h-1zM25 6h1v1h-1zM26 6h1v1h-1zM28 6h1v1h-1zM29 6h1v1h-1zM30 6h1v1h-1zM31 6h1v1h-1zM32 6h1v1h-1zM33 6h1v1h-1zM34 6h1v1h-1zM6 7h1v1h-1zM12 7h1v1h-1zM14 7h1v1h-1zM16 7h1v1h-1zM19 7h1v1h-1zM20 7h1v1h-1zM21 7h1v1h-1zM23 7h1v1h-1zM24 7h1v1h-1zM28 7h1v1h-1zM34 7h1v1h-1zM6 8h1v1h-1zM8 8h1v1h-1zM9 8h1v1h-1zM10 8h1v1h-1zM12 8h1v1h-1zM18 8h1v1h-1zM20 8h1v1h-1zM21 8h1v1h-1zM25 8h1v1h-1zM26 8h1v1h-1zM28 8h1v1h-1zM30 8h1v1h-1zM31 8h1v1h-1zM32 8h1v1h-1zM34 8h1v1h-1zM6 9h1v1h-1zM8 9h1v1h-1zM9 9h1v1h-1zM10 9h1v1h-1zM12 9h1v1h-1zM14 9h1v1h-1zM15 9h1v1h-1zM16 9h1v1h-1zM18 9h1v1h-1zM19 9h1v1h-1zM20 9h1v1h-1zM22 9h1v1h-1zM23 9h1v1h-1zM26 9h1v1h-1zM28 9h1v1h-1zM30 9h1v1h-1zM31 9h1v1h-1zM32 9h1v1h-1zM34 9h1v1h-1zM6 10h1v1h-1zM8 10h1v1h-1zM9 10h1v1h-1zM10 10h1v1h-1zM12 10h1v1h-1zM15 10h1v1h-1zM20 10h1v1h-1zM22 10h1v1h-1zM24 10h1v1h-1zM25 10h1v1h-1zM26 10h1v1h-1zM28 10h1v1h-1zM30 10h1v1h-1zM31 10h1v1h-1zM32 10h1v1h-1zM34 10h1v1h-1zM6 11h1v1h-1zM12 11h1v1h-1zM14 11h1v1h-1zM15 11h1v1h-1zM17 11h1v1h-1zM18 11h1v1h-1zM19 11h1v1h-1zM20 11h1v1h-1zM21 11h1v1h-1zM22 11h1v1h-1zM23 11h1v1h-1zM24 11h1v1h-1zM26 11h1v1h-1zM28 11h1v1h-1zM34 11h1v1h-1zM6 12h1v1h-1zM7 12h1v1h-1zM8 12h1v1h-1zM9 12h1v1h-1zM10 12h1v1h-1zM11 12h1v1h-1zM12 12h1v1h-1zM14 12h1v1h-1zM16 12h1v1h-1zM18 12h1v1h-1zM20 12h1v1h-1zM22 12h1v1h-1zM24 12h1v1h-1zM26 12h1v1h-1zM28 12h1v1h-1zM29 12h1v1h-1zM30 12h1v1h-1zM31 12h1v1h-1zM32 12h1v1h-1zM33 12h1v1h-1zM34 12h1v1h-1zM15 13h1v1h-1zM16 13h1v1h-1zM17 13h1v1h-1zM22 13h1v1h-1zM25 13h1v1h-1zM6 14h1v1h-1zM7 14h1v1h-1zM8 14h1v1h-1zM9 14h1v1h-1zM10 14h1v1h-1zM12 14h1v1h-1zM13 14h1v1h-1zM14 14h1v1h-1zM15 14h1v1h-1zM17 14h1v1h-1zM19 14h1v1h-1zM21 14h1v1h-1zM22 14h1v1h-1zM23 14h1v1h-1zM26 14h1v1h-1zM27 14h1v1h-1zM29 14h1v1h-1zM31 14h1v1h-1zM33 14h1v1h-1zM7 15h1v1h-1zM10 15h1v1h-1zM11 15h1v1h-1zM13 15h1v1h-1zM14 15h1v1h-1zM16 15h1v1h-1zM18 15h1v1h-1zM20 15h1v1h-1zM22 15h1v1h-1zM23 15h1v1h-1zM25 15h1v1h-1zM27 15h1v1h-1zM28 15h1v1h-1zM29 15h1v1h-1zM30 15h1v1h-1zM34 15h1v1h-1zM7 16h1v1h-1zM11 16h1v1h-1zM12 16h1v1h-1zM13 16h1v1h-1zM16 16h1v1h-1zM20 16h1v1h-1zM21 16h1v1h-1zM23 16h1v1h-1zM24 16h1v1h-1zM26 16h1v1h-1zM27 16h1v1h-1zM30 16h1v1h-1zM31 16h1v1h-1zM6 17h1v1h-1zM8 17h1v1h-1zM9 17h1v1h-1zM10 17h1v1h-1zM11 17h1v1h-1zM14 17h1v1h-1zM18 17h1v1h-1zM21 17h1v1h-1zM24 17h1v1h-1zM26 17h1v1h-1zM28 17h1v1h-1zM29 17h1v1h-1zM7 18h1v1h-1zM8 18h1v1h-1zM9 18h1v1h-1zM10 18h1v1h-1zM11 18h1v1h-1zM12 18h1v1h-1zM13 18h1v1h-1zM14 18h1v1h-1zM15 18h1v1h-1zM16 18h1v1h-1zM18 18h1v1h-1zM19 18h1v1h-1zM23 18h1v1h-1zM26 18h1v1h-1zM27 18h1v1h-1zM31 18h1v1h-1zM32 18h1v1h-1zM33 18h1v1h-1zM8 19h1v1h-1zM9 19h1v1h-1zM10 19h1v1h-1zM11 19h1v1h-1zM13 19h1v1h-1zM14 19h1v1h-1zM20 19h1v1h-1zM22 19h1v1h-1zM24 19h1v1h-1zM25 19h1v1h-1zM28 19h1v1h-1zM29 19h1v1h-1zM30 19h1v1h-1zM34 19h1v1h-1zM7 20h1v1h-1zM8 20h1v1h-1zM9 20h1v1h-1zM11 20h1v1h-1zM12 20h1v1h-1zM13 20h1v1h-1zM14 20h1v1h-1zM17 20h1v1h-1zM18 20h1v1h-1zM19 20h1v1h-1zM21 20h1v1h-1zM23 20h1v1h-1zM27 20h1v1h-1zM29 20h1v1h-1zM32 20h1v1h-1zM33 20h1v1h-1zM7 21h1v1h-1zM9 21h1v1h-1zM11 21h1v1h-1zM13 21h1v1h-1zM14 21h1v1h-1zM17 21h1v1h-1zM27 21h1v1h-1zM28 21h1v1h-1zM29 21h1v1h-1zM30 21h1v1h-1zM34 21h1v1h-1zM8 22h1v1h-1zM9 22h1v1h-1zM10 22h1v1h-1zM11 22h1v1h-1zM12 22h1v1h-1zM17 22h1v1h-1zM19 22h1v1h-1zM20 22h1v1h-1zM22 22h1v1h-1zM23 22h1v1h-1zM25 22h1v1h-1zM26 22h1v1h-1zM29 22h1v1h-1zM31 22h1v1h-1zM32 22h1v1h-1zM33 22h1v1h-1zM6 23h1v1h-1zM10 23h1v1h-1zM11 23h1v1h-1zM13 23h1v1h-1zM14 23h1v1h-1zM15 23h1v1h-1zM18 23h1v1h-1zM22 23h1v1h-1zM24 23h1v1h-1zM25 23h1v1h-1zM26 23h1v1h-1zM28 23h1v1h-1zM30 23h1v1h-1zM31 23h1v1h-1zM33 23h1v1h-1zM34 23h1v1h-1zM6 24h1v1h-1zM8 24h1v1h-1zM10 24h1v1h-1zM12 24h1v1h-1zM13 24h1v1h-1zM14 24h1v1h-1zM19 24h1v1h-1zM20 24h1v1h-1zM21 24h1v1h-1zM23 24h1v1h-1zM27 24h1v1h-1zM6 25h1v1h-1zM9 25h1v1h-1zM10 25h1v1h-1zM13 25h1v1h-1zM16 25h1v1h-1zM18 25h1v1h-1zM20 25h1v1h-1zM24 25h1v1h-1zM30 25h1v1h-1zM33 25h1v1h-1zM34 25h1v1h-1zM6 26h1v1h-1zM7 26h1v1h-1zM8 26h1v1h-1zM12 26h1v1h-1zM13 26h1v1h-1zM14 26h1v1h-1zM15 26h1v1h-1zM18 26h1v1h-1zM19 26h1v1h-1zM20 26h1v1h-1zM23 26h1v1h-1zM26 26h1v1h-1zM27 26h1v1h-1zM28 26h1v1h-1zM29 26h1v1h-1zM30 26h1v1h-1zM31 26h1v1h-1zM32 26h1v1h-1zM33 26h1v1h-1zM14 27h1v1h-1zM22 27h1v1h-1zM25 27h1v1h-1zM26 27h1v1h-1zM30 27h1v1h-1zM32 27h1v1h-1zM33 27h1v1h-1zM34 27h1v1h-1zM6 28h1v1h-1zM7 28h1v1h-1zM8 28h1v1h-1zM9 28h1v1h-1zM10 28h1v1h-1zM11 28h1v1h-1zM12 28h1v1h-1zM14 28h1v1h-1zM16 28h1v1h-1zM17 28h1v1h-1zM18 28h1v1h-1zM19 28h1v1h-1zM21 28h1v1h-1zM22 28h1v1h-1zM23 28h1v1h-1zM25 28h1v1h-1zM26 28h1v1h-1zM28 28h1v1h-1zM30 28h1v1h-1zM31 28h1v1h-1zM6 29h1v1h-1zM12 29h1v1h-1zM15 29h1v1h-1zM17 29h1v1h-1zM20 29h1v1h-1zM24 29h1v1h-1zM26 29h1v1h-1zM30 29h1v1h-1zM31 29h1v1h-1zM33 29h1v1h-1zM34 29h1v1h-1zM6 30h1v1h-1zM8 30h1v1h-1zM9 30h1v1h-1zM10 30h1v1h-1zM12 30h1v1h-1zM14 30h1v1h-1zM17 30h1v1h-1zM21 30h1v1h-1zM22 30h1v1h-1zM23 30h1v1h-1zM26 30h1v1h-1zM27 30h1v1h-1zM28 30h1v1h-1zM29 30h1v1h-1zM30 30h1v1h-1zM32 30h1v1h-1zM33 30h1v1h-1zM34 30h1v1h-1zM6 31h1v1h-1zM8 31h1v1h-1zM9 31h1v1h-1zM10 31h1v1h-1zM12 31h1v1h-1zM14 31h1v1h-1zM18 31h1v1h-1zM19 31h1v1h-1zM22 31h1v1h-1zM25 31h1v1h-1zM27 31h1v1h-1zM29 31h1v1h-1zM33 31h1v1h-1zM34 31h1v1h-1zM6 32h1v1h-1zM8 32h1v1h-1zM9 32h1v1h-1zM10 32h1v1h-1zM12 32h1v1h-1zM14 32h1v1h-1zM16 32h1v1h-1zM19 32h1v1h-1zM20 32h1v1h-1zM21 32h1v1h-1zM22 32h1v1h-1zM23 32h1v1h-1zM26 32h1v1h-1zM28 32h1v1h-1zM30 32h1v1h-1zM31 32h1v1h-1zM32 32h1v1h-1zM33 32h1v1h-1zM6 33h1v1h-1zM12 33h1v1h-1zM14 33h1v1h-1zM15 33h1v1h-1zM18 33h1v1h-1zM19 33h1v1h-1zM22 33h1v1h-1zM24 33h1v1h-1zM25 33h1v1h-1zM26 33h1v1h-1zM31 33h1v1h-1zM33 33h1v1h-1zM6 34h1v1h-1zM7 34h1v1h-1zM8 34h1v1h-1zM9 34h1v1h-1zM10 34h1v1h-1zM11 34h1v1h-1zM12 34h1v1h-1zM14 34h1v1h-1zM16 34h1v1h-1zM18 34h1v1h-1zM20 34h1v1h-1zM21 34h1v1h-1zM23 34h1v1h-1zM25 34h1v1h-1zM28 34h1v1h-1zM29 34h1v1h-1zM30 34h1v1h-1zM31 34h1v1h-1zM32 34h1v1h-1z"/></svg>
347
+ <span class="abdontext">
348
+ <span class="abdonlbl"><svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="11" fill="var(--accent)"/><path fill="#1a1206" d="M9.2 6.2h1.5V4.8h1.6v1.4h1.1V4.8H15v1.5c1.6.3 2.6 1.2 2.6 2.7 0 1-.5 1.8-1.3 2.2 1.2.4 1.9 1.3 1.9 2.6 0 1.8-1.3 2.8-3.2 3v1.5h-1.6v-1.4h-1.1v1.4h-1.6v-1.5H9.2v-1.8h1.1V8h-1.1zm2.6 3.9h1.7c1 0 1.6-.4 1.6-1.2 0-.8-.6-1.2-1.6-1.2h-1.7zm0 4.4h2c1.1 0 1.8-.5 1.8-1.3s-.7-1.3-1.8-1.3h-2z"/></svg> If BlockYard is useful, tips are welcome</span>
349
+ <code>bc1q249cv27lc2q7y0x53vkczgfvvgsjzhwxwv42gc</code>
350
+ <span class="abdonhint">scan, or click to copy</span>
351
+ </span>
352
+ </button>
353
+
354
+ <p class="abcredit">Entirely made by machines. Directed by human hands.</p>
355
+ </div>
356
+ </div>
357
+ </section>
358
+
359
+ <!-- ======================================================== MEMPOOL -->
360
+ <section class="page" data-page="mempool">
361
+ <div class="grid">
362
+ <!-- Two independent stacks, like the Overview (operator, 2026-09-11: "Too
363
+ much wasted space on this panel. We should compress, and add the
364
+ mempool display here too in a smaller panel"). The Pool usage card is
365
+ compact -- gauge beside its figures -- and the Block space viewer (the
366
+ same one as Overview, Mining and Block space) sits in the narrower
367
+ right column. -->
368
+ <div class="mpcols">
369
+ <!-- TWO COLUMNS, THE RIGHT ONE WIDER (operator, 2026-09-12: "Why can't we put Age
370
+ distribution next to Fee rate vs age under Fee estimates over time"). No reason at
371
+ all, and it balances the page: with three equal columns the figures-and-histogram
372
+ column ran 659px at a 1600px window while the other two sat at 375 and 392 -- a 284px
373
+ spread. The two distributions share a row under the time series instead.
374
+ These are flex columns, not a grid of cards: a grid stretches every card to its row's
375
+ tallest, which is what once held Mempool size open with ~130px of nothing inside it.
376
+ Here each card is exactly as tall as its contents. -->
377
+ <div class="ovcol">
378
+ <div class="card w3" id="mpUsageCard">
379
+ <h3>Pool usage <span class="sp"></span><span class="src">usage / maxmempool</span></h3>
380
+ <div class="mpusage">
381
+ <canvas class="chart meter" id="mpMeter"></canvas>
382
+ <dl class="kv" id="mpLimits"></dl>
383
+ </div>
384
+ </div>
385
+ <div class="card w6">
386
+ <h3>Fee rate distribution <span class="sp"></span><span class="src">sat/vB from getrawmempool (RPC)</span></h3>
387
+ <canvas class="chart" id="mpHistChart"></canvas>
388
+ <div class="note tiny faint" id="mpHistNote"></div>
389
+ </div>
390
+ </div>
391
+ <!-- No Block space viewer on this page (operator: "just remove the block space panel from
392
+ mempool entirely"). It lives on Overview, Block space and Mining; here it was the
393
+ tallest thing by a distance -- square, so its width was its height -- and it spent
394
+ most of this page's layout budget. -->
395
+ <div class="ovcol">
396
+ <div class="card w5">
397
+ <h3>Mempool size <span class="sp"></span><span class="src">count and bytes</span></h3>
398
+ <canvas class="chart" id="mpSizeChart"></canvas>
399
+ </div>
400
+ <div class="card w4">
401
+ <h3>Fee estimates over time <span class="sp"></span><span class="src">sat/vB</span></h3>
402
+ <canvas class="chart" id="mpFeeChart"></canvas>
403
+ </div>
404
+ <!-- the two distributions, side by side, under the time series -->
405
+ <div class="mprow">
406
+ <div class="card w6">
407
+ <h3>Fee rate vs age <span class="sp"></span><span class="src">sampled scatter</span></h3>
408
+ <canvas class="chart" id="mpScatter"></canvas>
409
+ <div class="note tiny faint" id="mpScatterNote"></div>
410
+ </div>
411
+ <div class="card w4">
412
+ <h3>Age distribution <span class="sp"></span><span class="src">seconds in pool</span></h3>
413
+ <canvas class="chart" id="mpAgeChart"></canvas>
414
+ </div>
415
+ </div>
416
+ </div>
417
+ </div>
418
+
419
+ <!-- WHAT THIS NODE CANNOT TELL US, as a strip rather than as rows. All three are absences:
420
+ two need the node's log (off on a monitor running RPC-only) and the third lists fields
421
+ getrawmempool omits. They are worth stating and not worth a third of the page. -->
422
+ <div class="card w4" id="mpAcceptCard">
423
+ <h3>Ingest &amp; rejects <span class="sp"></span><span class="src">from the node's log</span></h3>
424
+ <dl class="kv" id="mpAccept"></dl>
425
+ <canvas class="chart short" id="mpIngestChart"></canvas>
426
+ </div>
427
+
428
+ <div class="card w4" id="mpOrphansCard">
429
+ <h3>Orphan pool <span class="sp"></span><span class="src">held, parked and resolved</span></h3>
430
+ <dl class="kv" id="mpOrphans"></dl>
431
+ <canvas class="chart short" id="mpOrphanChart"></canvas>
432
+ </div>
433
+ <div class="card w4">
434
+ <h3>Fields this node does not report <span class="sp"></span><span class="src">getrawmempool shape</span></h3>
435
+ <div class="note" id="mpNotReported"></div>
436
+ </div>
437
+ </div>
438
+ </section>
439
+
440
+ <!-- ========================================================== PEERS -->
441
+ <!-- ========================================================== KIOSK -->
442
+ <!-- The 3D Markets board and the Block space board side by side (public/js/kiosk.js). -->
443
+ <section class="page" data-page="kiosk">
444
+ <div class="kiosk" id="kiosk">
445
+ <div class="kcol">
446
+ <div class="kpanel kmk">
447
+ <div class="khead"><b>Markets</b><span class="faint" id="kMkLabel"></span></div>
448
+ <div class="treemapwrap kboard"><canvas class="treemap" id="kMarkets"></canvas><div class="goggles-tip hidden"></div></div>
449
+ </div>
450
+ <!-- the price text in a panel of its own (operator: "Create a new panel for the price text to
451
+ exist in, and keep the markets in it's own panel") -->
452
+ <!-- PRICE AND THE BOOK, in one panel of its own (operator, 2026-09-12: "Make it a new
453
+ panel with price, obviously"). The chart needs real height, which it cannot get as a
454
+ strip under the markets board, so the panel is sized like the Block flow one. -->
455
+ <div class="kpanel kpp">
456
+ <div class="khead"><b>Price &amp; order book depth</b><span class="faint">five exchanges' tickers, and their books</span></div>
457
+ <div class="kprice" id="kPrice"></div>
458
+ <!-- The depth chart is a SIBLING of #kPrice, not a child: renderPriceInfo replaces that
459
+ element's innerHTML on every refresh, which would destroy a canvas inside it. -->
460
+ <div class="kdepthwrap"><canvas class="kdepth" id="kDepth"></canvas></div>
461
+ </div>
462
+ </div>
463
+ <div class="kcol">
464
+ <div class="kpanel ksp">
465
+ <div class="khead"><b>Block space</b><span class="faint">the next block's worth of the mempool</span></div>
466
+ <div class="treemapwrap kboard"><canvas class="treemap" id="kSpace"></canvas><div class="viewer-ctl"><div class="refresh-in" data-refresh></div><button type="button" class="refresh-now" data-refresh-now disabled>refresh now</button></div><div class="goggles-tip hidden"></div></div>
467
+ </div>
468
+ <div class="kpanel kbf">
469
+ <div class="khead"><b>Block flow</b><span class="faint">projected → being built → the chain</span></div>
470
+ <div id="kTrain" class="flowwrap"></div>
471
+ </div>
472
+ </div>
473
+ <button type="button" class="kfull" id="kFull" title="full screen">full screen</button>
474
+ </div>
475
+ </section>
476
+ <!-- TETRUST: a Tetris on the 3D engine (operator, 2026-09-12). The well is a board3d, so it wears
477
+ every display setting the Block space board does: stars, the galaxy, neon edges, the sheen. -->
478
+ <!-- BLOCKOUT (public/js/blockout.js, rules in breakout.js): Breakout on the block engine. The
479
+ court is a board3d, the bricks are one grid cell each, and the panel behind it is the same
480
+ sky the Block space board draws. -->
481
+ <section class="page" data-page="blockout">
482
+ <div class="tetrust" id="blockout">
483
+ <div class="card tetcard bocard">
484
+ <canvas class="tetsky" id="boSky"></canvas>
485
+ <h3>Blockout <span class="sp"></span><span class="src">break the wall — the bat follows your mouse</span></h3>
486
+ <div class="treemapwrap tetwell bowell idle" id="boWellWrap">
487
+ <canvas class="treemap" id="boWell"></canvas>
488
+ </div>
489
+ <div class="tetover" id="boOver">
490
+ <div class="tetmsg" id="boMsg">Blockout</div>
491
+ <div class="tetsub" id="boSub"></div>
492
+ <button type="button" class="btn primary" id="boResume">play</button>
493
+ </div>
494
+ </div>
495
+ <div class="card tethud">
496
+ <h3>Score</h3>
497
+ <div class="tetstats" id="boStats"></div>
498
+ <h3>Keys</h3>
499
+ <div class="tetkeys">move the mouse over the court, or ← → / A D · space or click serves · P pause · Enter play</div>
500
+ <h3>Switches</h3>
501
+ <div class="tetswitches">
502
+ <button type="button" class="tetsw" id="boStars" aria-pressed="false" title="the star field across the panel">★ stars</button>
503
+ <button type="button" class="tetsw" id="boGalaxy" aria-pressed="false" title="the spiral galaxy in that sky">🌀 galaxy</button>
504
+ <button type="button" class="tetsw" id="boNeon" aria-pressed="false" title="the wall under lit neon tubes">◉ neon</button>
505
+ <button type="button" class="tetsw" id="boSfx" aria-pressed="false" title="bat, bricks, walls and a lost ball">♫ sound</button>
506
+ </div>
507
+ <h3>High scores <span class="sp"></span><span class="src">this browser</span></h3>
508
+ <table class="tetscores" id="boScores"></table>
509
+ </div>
510
+ </div>
511
+ </section>
512
+ <!-- BLOCKANOID (public/js/blockanoid.js, rules in arkanoid.js): Arkanoid on the block engine.
513
+ A narrower, taller court than Blockout's; silver bricks survive a hit, gold survives every
514
+ hit, and a broken brick may drop a capsule. Vaus turns red while the laser is up. -->
515
+ <section class="page" data-page="blockanoid">
516
+ <div class="tetrust" id="blockanoid">
517
+ <div class="card tetcard bacard">
518
+ <canvas class="tetsky" id="baSky"></canvas>
519
+ <h3>Blockanoid <span class="sp"></span><span class="src">catch the capsules — Vaus follows your mouse</span></h3>
520
+ <div class="treemapwrap tetwell banwell idle" id="baWellWrap">
521
+ <canvas class="treemap" id="baWell"></canvas>
522
+ </div>
523
+ <div class="tetover" id="baOver">
524
+ <div class="tetmsg" id="baMsg">Blockanoid</div>
525
+ <div class="tetsub" id="baSub"></div>
526
+ <button type="button" class="btn primary" id="baResume">play</button>
527
+ </div>
528
+ </div>
529
+ <div class="card tethud">
530
+ <h3>Score</h3>
531
+ <div class="tetstats" id="baStats"></div>
532
+ <h3>Capsules</h3>
533
+ <!-- Each capsule has its own SHAPE on the court, not just its own colour, so the legend
534
+ names the shape too -- the swatch is a class, never a style attribute (CSP). -->
535
+ <ul class="caplegend">
536
+ <li><i class="capsw cap-laser">L</i><b>laser</b><span>twin cannons</span></li>
537
+ <li><i class="capsw cap-enlarge">E</i><b>wide</b><span>flat bar, capped ends</span></li>
538
+ <li><i class="capsw cap-catch">C</i><b>catch</b><span>ball in a cradle</span></li>
539
+ <li><i class="capsw cap-slow">S</i><b>slow</b><span>hollow frame</span></li>
540
+ <li><i class="capsw cap-disrupt">D</i><b>three balls</b><span>three beads</span></li>
541
+ <li><i class="capsw cap-player">P</i><b>a life</b><span>tall tower</span></li>
542
+ </ul>
543
+ <h3>Keys</h3>
544
+ <div class="tetkeys">move the mouse over the court, or ← → / A D · space or click serves · the laser fires itself while armed · P pause · Enter play</div>
545
+ <h3>Switches</h3>
546
+ <div class="tetswitches">
547
+ <button type="button" class="tetsw" id="baStars" aria-pressed="false" title="the star field across the panel">★ stars</button>
548
+ <button type="button" class="tetsw" id="baGalaxy" aria-pressed="false" title="the spiral galaxy in that sky">🌀 galaxy</button>
549
+ <button type="button" class="tetsw" id="baNeon" aria-pressed="false" title="the wall under lit neon tubes">◉ neon</button>
550
+ <button type="button" class="tetsw" id="baSfx" aria-pressed="false" title="Vaus, bricks, capsules, the laser and a lost ball">♫ sound</button>
551
+ </div>
552
+ <h3>High scores <span class="sp"></span><span class="src">this browser</span></h3>
553
+ <table class="tetscores" id="baScores"></table>
554
+ </div>
555
+ </div>
556
+ </section>
557
+
558
+ <section class="page" data-page="tetrust">
559
+ <div class="tetrust" id="tetrust">
560
+ <div class="card tetcard">
561
+ <canvas class="tetsky" id="tetSky"></canvas>
562
+ <h3>Tetrust <span class="sp"></span><span class="src">trust, but verify — every line you clear is a block you verified</span></h3>
563
+ <div class="treemapwrap tetwell idle" id="tetWellWrap">
564
+ <canvas class="treemap" id="tetWell"></canvas>
565
+ </div>
566
+ <div class="tetover" id="tetOver">
567
+ <div class="tetmsg" id="tetMsg">Tetrust</div>
568
+ <div class="tetsub" id="tetSub"></div>
569
+ <button type="button" class="btn primary" id="tetResume">play</button>
570
+ </div>
571
+ </div>
572
+ <div class="card tethud">
573
+ <h3>Score</h3>
574
+ <div class="tetstats" id="tetStats"></div>
575
+ <h3>Next</h3>
576
+ <div class="treemapwrap tetnext"><canvas class="treemap" id="tetNext"></canvas></div>
577
+ <h3>Keys</h3>
578
+ <div class="tetkeys">← → or A D move · ↑ W or X rotate · Z or Q rotate back · ↓ or S soft drop · space hard drop · P pause · Enter play</div>
579
+ <h3>Switches</h3>
580
+ <div class="tetswitches">
581
+ <button type="button" class="tetsw" id="tetStars" aria-pressed="false" title="the star field across the panel">★ stars</button>
582
+ <button type="button" class="tetsw" id="tetGalaxy" aria-pressed="false" title="the spiral galaxy in that sky">🌀 galaxy</button>
583
+ <button type="button" class="tetsw" id="tetMusic" aria-pressed="false" title="Korobeiniki, on oscillators">♪ music</button>
584
+ <button type="button" class="tetsw" id="tetSfx" aria-pressed="false" title="move, rotate, drop, clear, game over">♫ sound</button>
585
+ </div>
586
+ <h3>High scores <span class="sp"></span><span class="src">this browser</span></h3>
587
+ <table class="tetscores" id="tetScores"></table>
588
+ </div>
589
+ </div>
590
+ </section>
591
+
592
+ <!-- ======================================================== MARKETS -->
593
+ <!-- Exchange prices on the block-space board (public/js/markets.js, server/collect/markets.js). -->
594
+ <section class="page" data-page="markets">
595
+ <div class="grid">
596
+ <div class="card w12 mkcard">
597
+ <h3>Markets <span class="sp"></span><span class="src">public exchange APIs · hourly candles · fetched only while this tab is open</span></h3>
598
+ <div class="mkbar" id="mkBar"></div>
599
+ <!-- THE VISUALIZER FIRST (operator: "move the 3D View to the top of the markets display. That
600
+ should be our main selling point, is the spectacular visualizer") -->
601
+ <!-- ONE PRICE VIEW AT A TIME (operator, 2026-09-12: "have a selector for either the 3D
602
+ view or 2D view for price. Not both at the same time. Too much waste of space for
603
+ that screen"). Both elements stay in the page -- the toolbar switches between them
604
+ live, and markets.js skips the DRAW for whichever is hidden, so the unseen one costs
605
+ nothing per refresh. The 3D board keeps its place first; a test pins that order. -->
606
+ <!-- THE FIGURES SIT UNDER THE TOOLBAR IN BOTH VIEWS (operator, 2026-09-12: of the median,
607
+ spread, volume and reporting line, "that line is also missing on the 3D view"). It was
608
+ never missing -- it sat AFTER the board in the markup, so with the flat chart showing
609
+ it landed 37px under the toolbar, and with the 600px board showing it was pushed 647px
610
+ down and stranded below the picture. Measured both ways. One line of figures belongs
611
+ with the price, whichever way the price is drawn. -->
612
+ <div class="mksum" id="mkSummary"></div>
613
+ <!-- No legend beside the board (operator, 2026-09-12: "get rid of this. takes up too much
614
+ space on the markets page"). It was a 250px column of prose explaining that a green
615
+ body closed up and a red one closed down, next to a chart that shows exactly that.
616
+ The board takes the full width now. -->
617
+ <div class="mklayout" id="mkLayout">
618
+ <div class="treemapwrap mkboard"><canvas class="treemap" id="mkBoard"></canvas><div class="goggles-tip hidden"></div></div>
619
+ </div>
620
+ <canvas class="mkchart" id="mkChart"></canvas>
621
+ <div id="mkTable"></div>
622
+ <h3 class="mksub">Order book depth <span class="faint">cumulative bids and asks against price · each exchange's book, their total, and the total as it stood · change bars on the right, log scale</span></h3>
623
+ <div class="mkbar" id="mkDepthBar"></div>
624
+ <canvas class="mkchart mkdepth" id="mkDepth"></canvas>
625
+ <div class="note tiny faint" id="mkDepthNote"></div>
626
+ <div class="note tiny faint" id="mkNote"></div>
627
+ </div>
628
+ </div>
629
+ </section>
630
+
631
+ <!-- ======================================================= EXPLORER -->
632
+ <!-- Blocks, transactions and addresses from this node (public/js/explorer.js). The route is
633
+ on the URL: #explorer/block/<height>, #explorer/tx/<txid>, #explorer/address/<addr>. -->
634
+ <section class="page" data-page="explorer">
635
+ <div class="grid">
636
+ <div class="card w12 xcard">
637
+ <form id="xSearch" class="xsearch" role="search" autocomplete="off">
638
+ <input id="xQuery" type="search" spellcheck="false" placeholder="block height, block hash, transaction id or address" aria-label="search the chain">
639
+ <button type="submit">Search</button>
640
+ </form>
641
+ <div id="xBody"></div>
642
+ </div>
643
+ </div>
644
+ </section>
645
+
646
+ <section class="page" data-page="peers">
647
+ <div class="grid">
648
+ <!-- THE PEER TABLE IS THE PAGE (operator, 2026-09-11: "We need to really compress the
649
+ panels in this screen to show more data. I don't want to have to scroll down to see
650
+ the primary information for that tab"). Two Connections cards showed the same count,
651
+ two charts the same 24 hours, and a seven-row budget list answered "–" or "not
652
+ reported" to every row on this build, all before the first peer. Now one strip --
653
+ the count with what the node does report, then one short history -- and the table. -->
654
+ <div class="prtop">
655
+ <div class="card prsum">
656
+ <h3>Connections <span class="sp"></span><span class="src">getconnectioncount · getnetworkinfo</span></h3>
657
+ <div class="metric"><div><span class="v" id="prCount">–</span></div><div class="s" id="prSplit">–</div></div>
658
+ <dl class="kv prbudget" id="prBudget"></dl>
659
+ </div>
660
+ <div class="card">
661
+ <h3>Connection history <span class="sp"></span><span class="src">in / out over 24h</span></h3>
662
+ <canvas class="chart short" id="prChart"></canvas>
663
+ </div>
664
+ </div>
665
+ <div class="card w12 prpeers">
666
+ <h3>Peers <span class="sp"></span><span class="src">getpeerinfo, every 15 s · bytes and rates per connection</span></h3>
667
+ <div class="note tiny faint" id="prIdentityNote"></div>
668
+ <div id="prTable"></div>
669
+ </div>
670
+ </div>
671
+ </section>
672
+
673
+ <!-- ======================================================== NETWORK -->
674
+ <section class="page" data-page="network">
675
+ <div class="grid">
676
+ <div class="card w6">
677
+ <h3>Throughput <span class="sp"></span><span class="src">getnettotals (RPC)</span></h3>
678
+ <div class="metric"><div class="l">network in</div><div><span class="v" id="ovNetIn">–</span><span class="u" id="ovNetInU"></span></div>
679
+ <div class="s" id="ovDisk">–</div></div>
680
+ <canvas class="chart" id="ovNetChart"></canvas>
681
+ </div>
682
+ <div class="card w6">
683
+ <h3>Upload <span class="sp"></span><span class="src">what the node reports</span></h3>
684
+ <div class="note" id="ntUploadNote"></div>
685
+ <dl class="kv" id="ntRpc"></dl>
686
+ </div>
687
+ <div class="card w6">
688
+ <h3>Traffic accounting <span class="sp"></span><span class="src">cumulative</span></h3>
689
+ <dl class="kv" id="ntAccounting"></dl>
690
+ </div>
691
+ <div class="card w12">
692
+ <h3>Where each number comes from <span class="sp"></span><span class="src">explicit sources</span></h3>
693
+ <div class="note" id="ntSources"></div>
694
+ </div>
695
+ </div>
696
+ </section>
697
+
698
+ <!-- ========================================================= EVENTS -->
699
+ <section class="page" data-page="mining">
700
+ <div class="grid">
701
+ <!-- TWO INDEPENDENT STACKS (operator, 2026-09-11: "too much wasted space in this
702
+ display. Crunch down the panel sizes to fit more info"). One grid coupled the rows:
703
+ the square Mempool space viewer set the first row, so Block flow sat in a 591 px
704
+ card using ~273, and the landscape was stretched to the packages table. Stacks never
705
+ stretch a card to its neighbour; the explanatory Detailed card folds away. -->
706
+ <div class="mncols">
707
+ <div class="ovcol">
708
+ <div class="card">
709
+ <h3>Block flow <span class="sp"></span><span class="src">being assembled → <b>the current block</b> → history receding</span></h3>
710
+ <div id="mnFlow" class="flowwrap"></div>
711
+ </div>
712
+ <div class="mnpair">
713
+ <div class="card">
714
+ <h3>Packages in the block being built <span class="sp"></span><span class="src">ancestor graph from getrawmempool · depends</span></h3>
715
+ <div id="mnPackages"></div>
716
+ </div>
717
+ <div class="card">
718
+ <h3>Feerate landscape <span class="sp"></span><span class="src">the transactions selected for the next block</span></h3>
719
+ <canvas class="chart" id="mnFeeLandscape"></canvas>
720
+ <div class="note tiny faint">Buckets are sat/vB. Green means the bucket pays well above the block's own median, red below — the same shape a miner is looking at when it decides what still fits.</div>
721
+ </div>
722
+ </div>
723
+ <div class="card">
724
+ <h3>Pools in this window <span class="sp"></span><span class="src">coinbase tags, grouped by curated label</span></h3>
725
+ <div id="mnPools"></div>
726
+ </div>
727
+ </div>
728
+ <div class="ovcol">
729
+ <div class="card">
730
+ <h3>Mempool space <span class="sp"></span><span class="src">every rectangle is a waiting transaction: area = vbytes, colour = its feerate, richest first</span></h3>
731
+ <div class="treemapwrap sq">
732
+ <canvas class="treemap" id="gnMempoolTreemap"></canvas><div class="viewer-ctl"><div class="refresh-in" data-refresh></div><button type="button" class="refresh-now" data-refresh-now disabled>refresh now</button></div>
733
+ <div class="goggles-tip hidden"></div>
734
+ </div>
735
+ <div class="note tiny faint" id="gnMempoolNote"></div>
736
+ </div>
737
+ <div class="card">
738
+ <h3>Attribution <span class="sp"></span><span class="src">what we read and from where</span></h3>
739
+ <div id="mnCoverage"></div>
740
+ </div>
741
+ <details class="card mndetailed">
742
+ <summary><h3>Detailed <span class="sp"></span><span class="src">what this node can and cannot answer</span></h3></summary>
743
+ <div class="kv">
744
+ <dt>fee deployment by miner</dt><dd>above, per mined block</dd>
745
+ <dt>block fullness &amp; gaps</dt><dd>block train above</dd>
746
+ <dt>mempool feerate landscape</dt><dd>on the <b>Mempool</b> page (fee histogram + scatter)</dd>
747
+ <dt>CPFP / child-pays-for-parent clusters</dt><dd>shown above, <b>for the block under construction</b>.
748
+ The ancestor graph is <span class="mono">getrawmempool … depends</span> (indices into the selected
749
+ transactions) — 1,496 transactions, 19 multi-transaction packages and 40 transactions inside a package as
750
+ measured on 2026-09-09. It is <b>not</b> the whole mempool: <span class="mono">getrawmempool verbose</span> on
751
+ this build carries no <span class="mono">depends</span> and no <span class="mono">ancestorcount</span>, and
752
+ there is no <span class="mono">zmqpubsequence</span>, so the unselected pool cannot be clustered. The card
753
+ says which of those two it is showing rather than blurring them together.</dd>
754
+ <dt>1p1c ancestor packages</dt><dd><span class="warn">not shown.</span> The node reports accepted/failed counts for
755
+ 1p1c packages, but only in its own log lines, and log files are not a source this monitor reads. The package
756
+ view above is built from <span class="mono">getrawmempool … depends</span>, which is RPC.</dd>
757
+ </div>
758
+ </details>
759
+ </div>
760
+ </div>
761
+ </div>
762
+ </section>
763
+
764
+ <section class="page" data-page="logs">
765
+ <div class="grid">
766
+ <div class="card w12">
767
+ <h3>Monitor events <span class="sp"></span><span class="src">what this monitor observed and decided; the node's log is not a source here</span><span class="count faint tiny" id="lgCount"></span></h3>
768
+ <div class="rowform">
769
+ <input class="f grow minw-200" id="lgSearch" placeholder="filter text, e.g. orphan, unreachable, stored">
770
+ <select class="f" id="lgSev"><option value="">all severities</option><option value="warn">warn</option><option value="error">error</option><option value="info">info</option></select>
771
+ <select class="f" id="lgKind"><option value="">all kinds</option></select>
772
+ <span class="spacer grow"></span>
773
+ <button class="btn" id="lgClear">clear</button>
774
+ </div>
775
+ <div class="feed tall" id="lgFeed"></div>
776
+ </div>
777
+ </div>
778
+ </section>
779
+
780
+ <!-- ==================================================== NODE & RPC -->
781
+ <section class="page" data-page="node">
782
+ <div class="grid">
783
+ <div class="card w6">
784
+ <h3>RPC etiquette <span class="sp"></span><span class="src">what this app asks of the node</span></h3>
785
+ <dl class="kv" id="ndRpc"></dl>
786
+ <canvas class="chart short" id="ndLatChart"></canvas>
787
+ <div class="note tiny faint">The node's RPC server services one connection at a time on a single thread. This app keeps one request in flight, spaces them, batches multiple methods per connection, and drops a poll that could not answer in time to be true.</div>
788
+ </div>
789
+ <div class="card w6">
790
+ <h3>Poll cadence <span class="sp"></span><span class="src">configured vs actual</span></h3>
791
+ <table class="t" id="ndCadence"><thead><tr><th>tier</th><th class="r">configured</th><th class="r">actual</th><th class="r">last run</th></tr></thead><tbody></tbody></table>
792
+ <div class="note tiny faint" id="ndCadenceNote"></div>
793
+ </div>
794
+
795
+ <div class="card w6">
796
+ <h3>Data quality <span class="sp"></span><span class="src">known gaps, stated</span></h3>
797
+ <div id="ndQuality"></div>
798
+ </div>
799
+ <div class="card w3">
800
+ <h3>Monitor self-telemetry <span class="sp"></span><span class="src">this process</span></h3>
801
+ <dl class="kv" id="ndSelf"></dl>
802
+ <canvas class="chart short" id="ndSelfChart"></canvas>
803
+ </div>
804
+ <div class="card w3">
805
+ <h3>Log tail <span class="sp"></span><span class="src">follow state</span></h3>
806
+ <dl class="kv" id="ndTail"></dl>
807
+ </div>
808
+
809
+ <div class="card w6">
810
+ <h3>Panel sources <span class="sp"></span><span class="src">provenance</span></h3>
811
+ <div id="ndSources"></div>
812
+ </div>
813
+ <!-- THE NODE CONNECTION (operator, 2026-09-12: "Still left to do is a config connection in
814
+ the web settings. We have no way for users to configure a connection to their rpc
815
+ backend"). Test first, save second: the save button is disabled until a test has
816
+ actually ANSWERED, so nothing is written for an endpoint nobody reached. That is the
817
+ operator's own ordering.
818
+ No credential fields, deliberately: authentication is the node's own .cookie, found
819
+ from the data directory (server/config.js resolveCookie). Asking for an RPC password on
820
+ a page that is open access by default is not something to add quietly. -->
821
+ <div class="card w6">
822
+ <h3>Node connection <span class="sp"></span><span class="src">where this monitor looks</span></h3>
823
+ <!-- the placeholder deliberately is NOT a URL: web-contract.test.js refuses an http(s)
824
+ literal outside an <a href>, because in shipped markup that reads as a resource load
825
+ under `script-src/img-src 'self'`. It caught this exact line. -->
826
+ <div class="field"><label for="cnUrl">RPC URL</label><input class="f w-full" id="cnUrl" placeholder="http(s)://host:port" autocomplete="off" spellcheck="false"></div>
827
+ <div class="field"><label for="cnDatadir">data directory</label><input class="f w-full" id="cnDatadir" placeholder="/var/lib/bitcoin" autocomplete="off" spellcheck="false"></div>
828
+ <div class="rowform">
829
+ <select class="f" id="cnChain"><option value="main">main</option><option value="test">test</option><option value="signet">signet</option><option value="regtest">regtest</option></select>
830
+ <input class="f grow minw-140" id="cnLabel" placeholder="label, e.g. My node" autocomplete="off">
831
+ </div>
832
+ <div class="rowform">
833
+ <button class="btn" id="cnTest">test connection</button>
834
+ <button class="btn primary" id="cnSave" disabled>save</button>
835
+ </div>
836
+ <div class="note tiny" id="cnOut"></div>
837
+ <div class="note tiny faint">Authentication uses the node's <span class="mono">.cookie</span> from the data directory; no password is asked for or kept here. Saving writes this monitor's own configuration file and takes effect on restart.</div>
838
+ </div>
839
+ <div class="card w6">
840
+ <h3>RPC console <span class="sp"></span><span class="src">read-only allowlist</span></h3>
841
+ <div class="rowform">
842
+ <input class="f grow minw-180" id="rpcMethod" placeholder="method, e.g. getblockchaininfo">
843
+ <input class="f grow minw-140" id="rpcParams" placeholder='params JSON array, e.g. [6]'>
844
+ <button class="btn primary" id="rpcRun">call</button>
845
+ </div>
846
+ <div class="note tiny faint" id="rpcNote">Mutating, wallet- and peer-control methods are refused by this app regardless of what the node would do.</div>
847
+ <pre class="mono tiny output-box" id="rpcOut"></pre>
848
+ </div>
849
+ </div>
850
+ </section>
851
+
852
+ <!-- ========================================================== ADMIN -->
853
+ <section class="page" data-page="admin">
854
+ <div class="grid">
855
+ <div class="card w6">
856
+ <h3>Users <span class="sp"></span><span class="src">scrypt-hashed, never shown</span></h3>
857
+ <div class="scroll"><table class="t" id="adUsers"><thead><tr><th>user</th><th>role</th><th>created</th><th>last login</th><th></th></tr></thead><tbody></tbody></table></div>
858
+ <div class="hr"></div>
859
+ <div class="rowform">
860
+ <input class="f" id="newUser" placeholder="username" autocomplete="off">
861
+ <select class="f" id="newRole"><option value="viewer">viewer</option><option value="operator">operator</option><option value="admin">admin</option></select>
862
+ <button class="btn" id="btnGen">generate password</button>
863
+ </div>
864
+ <div class="note tiny" id="adGen"></div>
865
+ </div>
866
+
867
+ <div class="card w6">
868
+ <h3>Node actions <span class="sp"></span><span class="src">off unless enabled</span></h3>
869
+ <div class="note tiny" id="adActions"></div>
870
+ </div>
871
+
872
+ <div class="card w6">
873
+ <h3>Audit log <span class="sp"></span><span class="src">credentials use, without credentials</span></h3>
874
+ <div class="scroll lg"><table class="t" id="adAudit"><thead><tr><th>when</th><th>type</th><th>who</th><th>detail</th></tr></thead><tbody></tbody></table></div>
875
+ <div class="note tiny faint mt-6" id="adAuditSize"></div>
876
+ </div>
877
+
878
+ <div class="card w6">
879
+ <h3>Change own password <span class="sp"></span></h3>
880
+ <div class="field"><label>current password</label><input class="f w-full" type="password" id="pwCurrent" autocomplete="current-password"></div>
881
+ <div class="field"><label>new password</label><input class="f w-full" type="password" id="pwNew" autocomplete="new-password"></div>
882
+ <button class="btn primary" id="btnPw">change password</button>
883
+ <div class="note tiny faint mt-6">Changing it revokes every session, including this one.</div>
884
+ </div>
885
+ </div>
886
+ </section>
887
+ </main>
888
+ </div>
889
+
890
+ <div class="toast" id="toast"></div>
891
+ <script type="module" src="/js/app.js"></script>
892
+ </body>
893
+ </html>