blockyard 0.0.9 → 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 (58) hide show
  1. package/CHANGELOG.md +251 -1
  2. package/README.md +42 -23
  3. package/bin/blockyard.js +2 -1
  4. package/docs/API.md +16 -14
  5. package/docs/ARCHITECTURE.md +92 -5
  6. package/docs/CONFIGURATION.md +33 -26
  7. package/docs/GETTING-STARTED.md +5 -2
  8. package/docs/INSTALL.md +90 -33
  9. package/docs/MEASUREMENTS.md +147 -0
  10. package/docs/SECURITY.md +32 -15
  11. package/docs/TROUBLESHOOTING.md +35 -1
  12. package/docs/USER-GUIDE.md +266 -26
  13. package/package.json +1 -1
  14. package/public/404.html +1 -1
  15. package/public/css/app.css +306 -82
  16. package/public/donate-qr.png +0 -0
  17. package/public/index.html +295 -103
  18. package/public/js/agents.js +228 -51
  19. package/public/js/app.js +82 -8
  20. package/public/js/blockscene3d.js +179 -27
  21. package/public/js/charts.js +21 -21
  22. package/public/js/depthchart.js +31 -27
  23. package/public/js/details3d.js +1456 -71
  24. package/public/js/doom.js +31 -0
  25. package/public/js/dosaudio.js +48 -0
  26. package/public/js/dosgame.js +389 -0
  27. package/public/js/dosio.js +186 -0
  28. package/public/js/dospc.js +1353 -0
  29. package/public/js/dosworker.js +196 -0
  30. package/public/js/login.js +5 -0
  31. package/public/js/markets.js +46 -8
  32. package/public/js/mining.js +310 -32
  33. package/public/js/panels.js +14 -10
  34. package/public/js/pricechart.js +14 -13
  35. package/public/js/quake.js +20 -0
  36. package/public/js/settings.js +103 -21
  37. package/public/js/soundcard.js +459 -0
  38. package/public/js/theme.js +235 -0
  39. package/public/js/wolf3d.js +22 -0
  40. package/public/js/x86.js +1978 -0
  41. package/scripts/donate-qr.py +12 -9
  42. package/scripts/dos-bench.js +56 -0
  43. package/scripts/setup.js +34 -12
  44. package/scripts/shots.mjs +6 -0
  45. package/scripts/smoke.sh +1 -1
  46. package/scripts/tls.js +31 -0
  47. package/server/chain/index/build.js +21 -4
  48. package/server/collect/monitor.js +30 -1
  49. package/server/collect/network.js +295 -0
  50. package/server/config.js +46 -22
  51. package/server/http/api.js +49 -5
  52. package/server/http/games.js +77 -0
  53. package/server/http/server.js +8 -0
  54. package/server/main.js +53 -8
  55. package/server/tls/selfsigned.js +160 -0
  56. package/systemd/blockyard.service +7 -5
  57. package/docs/PRIVATE-LEADERBOARD.md +0 -230
  58. package/docs/STATE-2026-09-09.md +0 -200
@@ -68,11 +68,13 @@ Environment=BLOCKYARD_NODE_URL=http://127.0.0.1:8331
68
68
  Environment=BLOCKYARD_DATADIR=/home/bitcoin/.bitcoin
69
69
  Environment=BLOCKYARD_LOGFILE=/home/bitcoin/.bitcoin/debug.log
70
70
  Environment=BLOCKYARD_LOG_LEVEL=info
71
- # ACCESS: accounts are OFF by default, so anyone who can reach the bound addresses
72
- # reads this monitor (role "viewer": charts, feeds, read-only RPC console). User
73
- # administration, the audit trail and node writes stay closed to them. Uncomment to
74
- # require sign-in (accounts, roles, sessions, CSRF, per-user audit):
75
- #Environment=BLOCKYARD_AUTH=1
71
+ # ACCESS: hardened out of the box (2026-09-15) -- bound to 127.0.0.1 and sign-in ON;
72
+ # the first start prints the admin password once (or set BLOCKYARD_ADMIN_PASSWORD).
73
+ # To serve a LAN address put it in config/local.json (`server.hosts`) or uncomment
74
+ # the first line; to open the monitor to readers with no account, the second --
75
+ # then anyone who can reach the bound addresses reads it as role "viewer".
76
+ #Environment=BLOCKYARD_BIND=0.0.0.0
77
+ #Environment=BLOCKYARD_AUTH=0
76
78
  #
77
79
  # Node writes stay off, and while accounts are off they stay off even if actions are
78
80
  # enabled -- a write with no identity behind it is not attributable. Enabling needs
@@ -1,230 +0,0 @@
1
- # Blockchain leaderboards for the Diversions — scoping report
2
-
3
- **Private local document.** Gitignored via `docs/PRIVATE-*.md`. Not a decision, not a commitment —
4
- a scoping of what this would actually take, written 2026-09-13.
5
-
6
- > Note on the privacy guard: `test/privacy.test.js` scans `git ls-files`, so an ignored file is
7
- > outside it. That is how a real LAN address reached a commit earlier today. Nothing sensitive
8
- > should go in here on the assumption a test will catch it.
9
-
10
- ---
11
-
12
- ## The short answers
13
-
14
- | question | answer |
15
- |---|---|
16
- | Do we need a Bitcoin address? | **Only if you want on-chain writes or signed identity.** A working leaderboard needs neither. |
17
- | How do people submit scores? | **A replay, not a score.** The server re-plays the game and computes the score itself. |
18
- | Can we do it for free? | **Yes, entirely** — if "use the blockchain" means *seed from a block hash and anchor by timestamp*. **No** if it means *one transaction per score*. |
19
- | What is the hard part? | **Not the blockchain.** It is proving a score was earned rather than typed into a POST body. |
20
-
21
- ---
22
-
23
- ## 1. The insight this whole design rests on
24
-
25
- The three games were built with a pure-rules / screen split, and the rules are **deterministic by
26
- construction**. This was done for testability, but it is exactly what a trustworthy leaderboard
27
- needs.
28
-
29
- Verified headless under Node on 2026-09-13:
30
-
31
- ```
32
- tetris.js newGame(seed) with a seeded rng(seed); newGame(12345) twice → identical queue
33
- arkanoid.js hash01(key) everywhere, NO Math.random — capsules, minion lanes, drift phases
34
- breakout.js launch angle is an argument, not a roll
35
- all three "no DOM, no canvas, no clock"; they import and run fine in plain Node
36
- ```
37
-
38
- Entry points for a replay: `step(g, dtMs)` (breakout, arkanoid), `tick(g)` / `move` / `rotate` /
39
- `hardDrop` (tetris). `tetrust.js` already drives the rules from a **fixed-step accumulator**
40
- (`while (G.acc >= ms)`), so a game is already a discrete sequence of steps, not a wall-clock
41
- animation.
42
-
43
- **Therefore the server can replay a submitted game and recompute the score.** That is real
44
- anti-cheat, and it does not involve Bitcoin at all.
45
-
46
- ## 2. What "use the blockchain" can usefully mean here
47
-
48
- Four options, ranked by what they actually buy:
49
-
50
- ### (a) Block hash as the game seed — free, and the best fit
51
-
52
- The tip hash at game start becomes the seed. This gives three properties for nothing:
53
-
54
- - **Unpredictable** — nobody can pre-compute a favourable piece order, because nobody knows block
55
- N+1's hash. (Miners have a marginal grinding advantage; irrelevant at this stake.)
56
- - **A lower time bound** — a game seeded from block N provably was not played before block N. That
57
- is a real timestamp, from our own node, at zero cost.
58
- - **A fair shared round** — everyone playing "the block 966,781 round" gets the same piece order, so
59
- scores are comparable in a way they never are with per-player seeds. This is the genuinely novel
60
- bit: a *per-block tournament*.
61
-
62
- We already have the data: `/api/blocks` returns hashes, and the monitor is watching a node anyway.
63
-
64
- ### (b) OpenTimestamps anchoring — free, proves *when*
65
-
66
- Hash the leaderboard state periodically and submit the digest to public OTS calendar servers, which
67
- aggregate thousands of digests into one transaction. Produces a Bitcoin-anchored proof that the
68
- leaderboard said X at time T, and costs nothing because the calendar operator pays the fee.
69
-
70
- Cost: one outbound HTTPS call to a third party, which conflicts with this project's "outbound
71
- connections are limited and on demand" stance — it would need to be opt-in and documented in
72
- `docs/SECURITY.md` alongside the exchange APIs.
73
-
74
- ### (c) OP_RETURN per score — cheap, not free, and probably wrong
75
-
76
- Real numbers from the node right now: fee estimates ≈ **5.65e-06 BTC/kvB ≈ 0.57 sat/vB**, relay
77
- floor 1 sat/vB. A minimal OP_RETURN transaction is ~200 vB → **~200 sats ≈ 15¢** at $77k.
78
-
79
- But: needs a funded wallet, key custody, a UTXO set to manage, change handling, fee bumping when the
80
- mempool moves, and it puts arcade scores in every archival node on Earth, for ever. It also requires
81
- passing the node-writes gate, which currently needs four independent yeses and is off by default.
82
-
83
- **Recommendation: no.** Use (a) for fairness and (b) for timestamping.
84
-
85
- ### (d) Signed scores for pseudonymous identity — free via RPC, but legacy addresses only
86
-
87
- A player signs their submission with a Bitcoin key; the address is their leaderboard identity. No
88
- funds, no on-chain footprint.
89
-
90
- **Partly wrong as first written, corrected 2026-09-13 after the operator asked "why do we need
91
- crypto primitives — can't we just use the bitcoind rpc?".** They were right.
92
-
93
- *Verifying* a user's signature needs no crypto code at all: **`verifymessage` is already permitted
94
- by our own allowlist** and the node does the pubkey recovery. Confirmed it genuinely evaluates
95
- rather than stubbing — a malformed signature errors `-3 Malformed base64 encoding`, and a bech32
96
- address errors `Address does not refer to key`.
97
-
98
- What survives is an **asymmetry**: the node can verify, but it cannot sign *for us*. The Umbrel has
99
- no wallet loaded (`listwallets` → `[]`), `signmessage` is in our own deny list as a wallet mutator,
100
- and asking a user's node to hold the monitor's identity is the wrong layer anyway. So:
101
-
102
- - **users proving they control an address** → pure RPC, no crypto, no dependency;
103
- - **the monitor holding its own identity and signing attestations** → local keys, which is a
104
- custody decision rather than a coding one.
105
-
106
- Node 22 does have enough for the second if wanted: secp256k1 DER sign/verify, `ripemd160`, and
107
- therefore hash160 — a P2WPKH address derives zero-dependency (verified: produced a valid bech32
108
- address with a ~40-line encoder). It is *recovery* from a compact signature that Node lacks, and
109
- only the monitor-side signing path would need it.
110
-
111
- **THE REAL CONSTRAINT IS ADDRESS TYPE, NOT CRYPTO.** Measured against the node on 2026-09-13:
112
-
113
- | address | `verifymessage` |
114
- |---|---|
115
- | P2PKH `1...` | **works** — returned `false` for a wrong signature, i.e. it evaluated |
116
- | P2SH `3...` | error: `Address does not refer to key` |
117
- | P2WPKH `bc1q...` | error: `Address does not refer to key` |
118
- | P2TR `bc1p...` | error: `Address does not refer to key` |
119
-
120
- Core's `verifymessage` implements the old "Bitcoin Signed Message" scheme, which is defined only
121
- for P2PKH. **Most modern wallets — including the one on an Umbrel — hand out `bc1q` or `bc1p`
122
- addresses, and those users cannot sign this way at all.** That is the main obstacle to the
123
- user-identity design, and no amount of our own code fixes it, because the limitation is in the
124
- signing standard rather than in us.
125
-
126
- Options, in order of how much they cost:
127
-
128
- 1. **Accept legacy-only**, and say so in the UI: "paste a `1...` address". Many wallets can still
129
- produce one; Sparrow, Electrum and Core itself can sign with a legacy key. Free, honest, and
130
- excludes some users.
131
- 2. **BIP-322** — the modern generalised signing scheme covering segwit and taproot, and what `bc1`
132
- wallets actually implement. **Checked, and the answer is no:** Core 31.1.0 (`/Satoshi:31.1.0/`)
133
- exposes `verifymessage`, `signmessage`, `signmessagewithprivkey`, `signrawtransactionwithkey`,
134
- `signrawtransactionwithwallet` and `enumeratesigners` — and **no BIP-322 method of any name**.
135
- So the node cannot verify a segwit or taproot signature for us. Supporting `bc1` identities means
136
- implementing BIP-322 verification ourselves, which needs precisely the secp256k1 primitives the
137
- RPC route was chosen to avoid. That is a substantial piece of work and a dependency-rule
138
- conversation, not an afternoon.
139
- 3. **Do not use Bitcoin signing for identity at all** — a chosen display name plus the replay proof.
140
- The replay is what makes a score trustworthy; the address only says *who*. If the leaderboard is
141
- local or shared-by-export, a name is sufficient and costs nothing.
142
-
143
- ## 3. Recommended design
144
-
145
- **Decision taken 2026-09-13: identity means _users prove who they are_** — the monitor holds no
146
- key, signs nothing, and custodies nothing. Everything below assumes that.
147
-
148
- **Per-block tournaments, verified by replay, identified by address, anchored by timestamp.**
149
-
150
- 1. A round opens when a block lands. Its hash seeds every game in that round.
151
- 2. The player plays locally, exactly as now.
152
- 3. On game over, the client submits a **replay**: `{ game, seedBlock, seedHash, steps[], claimedScore }`
153
- where `steps[]` is the discrete input sequence the fixed-step loop already produces.
154
- 4. The server imports the same pure rules module, replays the steps, and computes the score itself.
155
- The claimed score is ignored except as a cross-check — a mismatch is a rejected submission.
156
- 5. Accepted entries join the round's board. When the round closes (next block), the board is hashed
157
- and optionally OTS-anchored.
158
-
159
- **Identity, if a player wants one** (optional — an anonymous entry is still replay-verified):
160
-
161
- - The server issues a challenge string tied to the round (`blockyard:<round>:<nonce>`).
162
- - The player signs it in their own wallet and pastes address + signature.
163
- - We call `verifymessage` — already permitted by the allowlist, no crypto code, no key on our box.
164
- - `validateaddress` first, so a `bc1` address is refused with *"legacy `1...` addresses only, see
165
- BIP-322 note"* rather than a confusing `Address does not refer to key`.
166
-
167
- Two allowlist additions are needed, one line each: `validateaddress` and `deriveaddresses`. Both
168
- are read-only and currently denied only because nothing had asked for them.
169
-
170
- **Testing this path needs no wallet.** `signmessagewithprivkey` takes a WIF and a message and
171
- returns a signature, so a test can generate a genuine legacy-address signature as a fixture and
172
- assert that our verification accepts it and rejects a tampered message — without depending on a
173
- wallet existing on whatever node the suite runs against. That matters: the Umbrel has none
174
- (`listwallets` → `[]`), and the fake node in `scripts/fake-node.js` would otherwise have to grow a
175
- signing implementation.
176
-
177
- Why this is worth doing: the leaderboard is **verifiable by anyone**. Given the replay, any third
178
- party can recompute the score using the same public rules file. That is a much stronger claim than
179
- most leaderboards can make, and it comes from the determinism that already exists.
180
-
181
- ## 4. What it actually costs to build
182
-
183
- | piece | size | notes |
184
- |---|---|---|
185
- | Record inputs in the three screen modules | **M** | The fixed-step loop already exists; capture `(stepIndex, action)` pairs. Arkanoid is the fiddliest — mouse position per step needs quantising or it is unreplayable. |
186
- | Headless replay harness | **S** | The rules already import under Node. Mostly a loop and a score readout. |
187
- | Replay determinism tests | **M** | The real work: proving client and server agree on every float. **See §5.** |
188
- | Submission API + storage | **S** | One POST, one GET, a JSON file per round. |
189
- | Round lifecycle from block events | **S** | The monitor already emits new-tip events. |
190
- | Leaderboard UI | **M** | A page, a per-round board, a "verify this replay yourself" button. |
191
- | OTS anchoring | **S** | Optional, opt-in, one HTTPS call. |
192
- | **Total** | **~2-4 days** | Assuming local/self-hosted scope. |
193
-
194
- ## 5. The risks, honestly
195
-
196
- - **Floating-point determinism is the sharp edge.** `breakout.js` and `arkanoid.js` advance ball
197
- physics with floats. Same JS engine, same order of operations, same result — but client and server
198
- are *different Node/browser builds*, and any divergence makes an honest player's replay fail
199
- verification. **This must be proven before building anything else**: record N real games in a
200
- browser, replay them server-side, and assert bit-identical scores. If it does not hold, the fix is
201
- integer or fixed-point physics, which is a rules rewrite. Tetris is much safer here — it is
202
- integer grid logic.
203
- - **Replay size.** A ten-minute Arkanoid game at 60 steps/second is ~36,000 steps. Compressible
204
- (most steps are "no input"), but it is not a 20-byte score.
205
- - **A leaderboard is inherently shared, and this app is not.** Blockyard is a single-user LAN
206
- monitor that ships open-access with a `viewer` ceiling. A *global* leaderboard means **you run a
207
- hosted service** — with moderation, abuse handling, data retention, and an endpoint strangers
208
- POST to. That is a different product with different obligations, not a feature of this one.
209
- Three honest scopes:
210
- - **Local only** — verified replays, your own high scores, no server. Nearly free to build.
211
- - **Shared export** — a signed/anchored replay file you can hand to someone else to verify.
212
- - **Hosted leaderboard** — the real product, and the one with ongoing cost and duty of care.
213
- - **Replay does not stop a bot.** It proves the score was *achievable under the rules*, not that a
214
- human earned it. A perfect Tetris bot submits perfectly valid replays. Per-block rounds limit the
215
- damage (everyone gets one shot at that seed) but do not eliminate it.
216
- - **Scope creep into custody.** The moment there is a funded address, this stops being a monitor and
217
- becomes something holding other people's money-adjacent state. Option (c) is where that starts,
218
- which is the strongest argument against it.
219
-
220
- ## 6. Recommendation
221
-
222
- 1. **Spike the determinism question first** (half a day). Everything else is wasted if browser and
223
- server disagree on ball physics.
224
- 2. Build **local-only** verified replays with block-hash seeds. Free, no address, no third party, no
225
- hosted service, and it delivers the interesting half: provably fair rounds and self-verifying
226
- scores.
227
- 3. Add **OTS anchoring** only if "provably at this time" matters to you. Opt-in, documented as an
228
- outbound connection.
229
- 4. **Do not** put scores on-chain per-game, and **do not** introduce a funded wallet.
230
- 5. Treat a hosted global leaderboard as a separate product decision, not a Diversions feature.
@@ -1,200 +0,0 @@
1
- # State of the project — 2026-09-09 (handoff for an agent with a real browser)
2
-
3
- > **Superseded 2026-09-14, names only.** This is a dated record and its measurements stand as
4
- > written. The names in it are the ones in use that day and are no longer current: the project
5
- > is **BlockYard** (`/storage/blockyard`, systemd `blockyard.service`, env vars `BLOCKYARD_*`,
6
- > default web port 21000), and the node it watches is Bitcoin Core. `bmc-port-guard.sh` and
7
- > `/etc/ssl/bmc-local/ca.crt` are real artefacts on the operator's machine and keep their names.
8
-
9
- Written by the session that built the mining/visualisation layer. Everything below was
10
- run on this box; the things I could **not** do are listed explicitly at the end. Read
11
- `AGENTS.md` first for the rules (zero dependencies, RPC-only sources, never fabricate a
12
- figure, a chart never erases itself).
13
-
14
- ## What is running
15
-
16
- | Thing | Where | Note |
17
- |---|---|---|
18
- | bmcmonitor | `https://<LAN address>:8088` | systemd `bmcmonitor.service`, TLS via a **local CA** (`/etc/ssl/bmc-local/ca.crt`), binds the **LAN address only** — loopback, docker bridges and the tailnet address all refuse, by design |
19
- | Node watched | `bmc-main`, RPC `127.0.0.1:8331` | the node's own esplora REST is on `127.0.0.1:3005` |
20
- | Log tailing | **OFF** (default since today) | the UI reads RPC only. `BMC_MON_LOG_SOURCE=1` re-enables it for parser work; do not add panels that need it |
21
- | Block-chain node under dev | `/storage/bitcoinmachinecode` | a *different* session works there; do not restart or reindex it casually |
22
-
23
- Import the CA or the browser will refuse the certificate. After any deploy, hard-reload:
24
- asset URLs are stamped `?v=<build>` and the stamp changes when files change, so a tab
25
- holding an old tab keeps running old code — that has already produced one false report
26
- today ("still renders nothing" while the fix was on disk and undeployed).
27
-
28
- ## Verify, don't trust this document
29
-
30
- ```bash
31
- cd /storage/bmcmonitor
32
- npm test # unit + render + contract tests
33
- bash scripts/smoke.sh # 109 checks against a real server
34
- npm run counts # do the documented counts match the suite?
35
- LAN=$(python3 -c "import json;print(json.load(open('config/local.json'))['server']['hosts'][0])")
36
- BMC_MON_BASE=https://$LAN:8088 BMC_MON_CA=/etc/ssl/bmc-local/ca.crt npm run render:live
37
- sudo systemctl restart bmcmonitor # deploy after editing public/** or server/**
38
- sudo journalctl -u bmcmonitor -f # server-side view; the UI records to it
39
- ```
40
-
41
- `render:live` renders **every page** under the DOM stub against the running monitor and
42
- fails if an asserted card is empty. Today it caught, in order: an uncaught `ReferenceError`
43
- that killed the entire Overview (two cards "rendered nothing" because one line above them
44
- threw), a `fmt is not defined` crash on the Node page, and a card that no test had ever
45
- reached.
46
-
47
- ## What was built today (mining + visualisation)
48
-
49
- - **Mining page** (`#mining`): block flow (construction → tip → history, the tip explicitly
50
- labelled `current tip`, colour = age verdict), ghost cards for the next blocks marked
51
- `not yet assembled`/`estimate`, ancestor-package table (CPFP shape from
52
- `getblocktemplate…depends`, with `depends` being **indices into the array**, not txids),
53
- and two treemaps.
54
- - **Treemaps** (`public/js/goggles.js`): one rectangle per transaction, area = vbytes,
55
- colour = the feerate it pays, richest-first. The canvas is **the block**: a 14%-full block
56
- looks 14% full and the rest is drawn blank and labelled. Cells are bounded (400) and the
57
- remainder collapses into ONE aggregate cell so the block is always whole.
58
- - **Tip freshness** is judged against the chain's own measured block interval, not a fixed
59
- clock: amber past ~1× the average gap, red past ~1.5×, floors at 4/8 min. Arrival is only
60
- counted when the page **witnessed** the height change — a first paint must not claim a
61
- fresh tip (that bug coloured a 14-minute-old block green).
62
- - **Overview** gained a compact copy of the block map (`ovGnTreemap`, `w4`, `.treemap.sm`).
63
-
64
- ## Bugs I fixed, so you don't re-diagnose them
65
-
66
- - `rows` TDZ in `renderOverview` — two `rows` in one function; killed the whole Overview
67
- render (the Fees card and the "mempool vs blocks" card were collateral).
68
- - `fmt is not defined` in `panels.js:drawSelf` — the Node page crashed.
69
- - Treemap `drawFrame` referenced `label` without destructuring → **every partly-filled
70
- block threw at paint time and the canvas stayed blank**.
71
- - The treemap tween re-requested animation frames whenever the clock failed to advance
72
- (hidden/throttled tab) → unbounded loop. Now a frame budget + stalled-clock check, and
73
- the settled layout always draws.
74
- - **CSP**: the policy is `style-src 'self'` with no `style-src-attr` allowance (correct,
75
- tightened earlier today). My new markup emitted `style="..."` and the browser blocked
76
- **several hundred** applications, so the train and maps lost every colour. All of it now
77
- travels as `data-pool` / `data-rate` / `data-w` / `data-delay` / `data-rail` and lands on
78
- `el.style` through the CSSOM (`applyMiningStyles` in `mining.js`, `applyDataSizes` in
79
- `app.js`). The palette index comes from our own array — a pool name from the node cannot
80
- become CSS. `test/csp.test.js` now fails if any JS-built markup carries a style attribute.
81
- - **The Fees card** was found with `getElementById('ovFees').closest('.card')`, and the DOM
82
- stub answers `closest()` with `null` — so **no test ever exercised that card**. It is now
83
- `#ovFeesCard`, addressed by id, and asserted in `test/web-contract.test.js`.
84
-
85
- ## Known, open, or unverified — this is what a browser is for
86
-
87
- 1. **Nothing has been seen rendered.** No real-browser harness exists. Layout, colour
88
- legibility, overlap, and "does the treemap read as a block" are unverified by me.
89
- 2. **Treemaps at `w4`** (33%) on Mining, `w4` on Overview: is 168px/240px high enough for
90
- the labels, and is the "block unfilled" band legible at that width?
91
- 3. **Legend/label collisions** inside rectangles (rate + `k` vbytes text) at small cell
92
- sizes, and the `selection ends` / `everything fits in one block` label near the right
93
- edge.
94
- 4. **Hover tooltips** (`.goggles-tip`) are positioned `left/bottom` — check they don't
95
- cover the cursor's cell or overflow the card.
96
- 5. **Motion**: the map tweens only when the Mining page is open and the template refetches
97
- (on load + every 20 s). Confirm the tween is visible, and that `prefers-reduced-motion`
98
- snaps without losing information.
99
- 6. **`app.js` boot throws under the DOM stub** (`TypeError` at `boot()`, harness-only). Real
100
- browsers don't hit it; it means a stub element is missing, and it will keep biting anyone
101
- extending the harness.
102
- 7. Ghost-card forward capacity numbers are **queue-depth estimates**, not node data.
103
- 8. No TLS certificate automation (self-signed local CA), and `:8999` (mempool-backend API)
104
- is loopback-only by a netfilter rule — separate from this app.
105
-
106
- ## Guardrails that will bite if you ignore them
107
-
108
- - **Zero dependencies.** No `npm install`. Node builtins and hand-written canvas only.
109
- - **No host identity in committed files.** `test/privacy.test.js` derives the username,
110
- hostname forms and interface addresses at run time and fails on them — it caught a
111
- hardcoded LAN address in one of my scripts today. Use RFC 5737 documentation ranges in
112
- docs and tests, and read addresses from config/env at run time.
113
- - **Test counts are generated** (`npm run counts:fix`). Never type them.
114
- - **Every claim in a comment must be a measurement** with a number and a date. "Seems" is
115
- not a justification, and this project has twice been burned by a check that passed for
116
- the wrong reason (a vacuous assertion, a stub that answered `null`).
117
- - A chart **never erases itself** to show a status; absence renders as `–` with a reason,
118
- and stale data renders visibly stale.
119
- - Absent data is **never** filled with a guess: no per-tx `depends` in
120
- `getrawmempool` on this node, so cluster analysis is only drawn from
121
- `getblocktemplate`, and the panel says which of the two it is.
122
-
123
- ---
124
-
125
- ## Measured in a real browser (2026-09-10, headless Chromium over CDP)
126
-
127
- `scripts/browser-check.mjs` drives Chromium over CDP with Node's built-in WebSocket: it
128
- loads the page, samples **canvas pixels with getImageData**, reads inline vs computed CSSOM
129
- values, measures element rects, collects console/exceptions, and can pass the screenshot to
130
- the local vision model. This is the first time any of this has been looked at rather than
131
- inferred. It found five defects that 349 passing tests did not.
132
-
133
- ### Found and fixed
134
- 1. **Shared animation handle.** `goggles.js` kept one module-level `raf` for both maps, so
135
- each canvas's render cancelled the *other* one's pending frame — and the first frame was
136
- drawn only inside that callback. The loser painted **nothing**: `__hasData: true`, 401
137
- rects laid out, 68% of the canvas covered on paper, **zero painted pixels**. Now the
138
- handle is per-canvas (`c.__raf`) and the settled layout is painted before any animation.
139
- 2. **Tween grew cells from zero size.** With a snapshot arriving every second, the chart
140
- spent most of its life mostly-cleared with 1px rectangles. Now position interpolates and
141
- **size never does**; new cells fade in with alpha. A tween must be the difference between
142
- two visible states, not the thing that makes pixels exist.
143
- 3. **`style-src` was silently vetoing my own markup** (several hundred blocked
144
- applications): all `style="..."` removed in favour of `data-pool` / `data-rate` /
145
- `data-w` / `data-h` / `data-delay` / `data-rail` + CSSOM. Guarded by a test that fails if
146
- any JS-built markup carries a style attribute.
147
- 4. **`.closest('.card')`** made the Fees card invisible to every test (the DOM stub answers
148
- `null`), so it could render nothing forever. Now addressed by `#ovFeesCard`.
149
- 5. **`rows` TDZ** in `renderOverview` killed the whole Overview. Also: the earlier "fixed"
150
- report was false because the fix was never **deployed** — deploy is part of a fix.
151
-
152
- ### Still broken, with the measurements
153
- - **The Mining page block map still paints nothing** (`gnTreemap`: `rects: 401`,
154
- `distinctColours: 0`, `paintedPixelsPct: 0`) while the mempool map on the same page paints
155
- 100% with the identical code path and the same rect count. I did not find the cause. Start
156
- by comparing what differs at `drawFrame` time between the two — `geom.unused` is set for
157
- the block map and absent for the mempool map, which changes `region`; instrument
158
- `drawFrame` with a counter and a sample of the first rect's `fillStyle`.
159
- - **The style pass is only half applying.** In the browser: `.bdot` inline style is
160
- `background: rgb(95, 176, 201)` with **no `--pool`**, and `.bfill span` has `background`
161
- but **no width**, while `document.querySelectorAll('[data-w]')` counts 27 of which **2**
162
- have an inline width (the sync hero's, applied by app.js). So `applyMiningStyles` is
163
- running an older code path than the one on disk and served (`setProperty` and
164
- `applySizes` are both present in the served bytes, and `Cache-Control: no-cache`). That
165
- contradiction is unresolved — check whether the module the browser evaluates is the
166
- module the server sent (compare `import.meta` resolution / a marker constant), before
167
- touching the style code again.
168
- - **The flow row overflows its card** and is cut off: `.flow` scrollWidth 2236 inside
169
- clientWidth 1369. I added `.flow { width: max-content }` + `.flowwrap { overflow-x: auto }`;
170
- the browser still measured clipping after the change, so either the CSS did not apply as
171
- written or the grid parent constrains it. Re-measure `.flowwrap` rather than `.flow`.
172
-
173
- > **Superseded 2026-09-14:** the first two readings above were the harness measuring the
174
- > previous build, not the page — `Page.navigate` to a URL differing only in its fragment does not
175
- > reload the document (`docs/RULES.md` rule 25). Re-run against the current build on 2026-09-10,
176
- > `gnTreemap` and `gnMempoolTreemap` both painted 100% of their pixels.
177
-
178
- ### Notes on the harness itself
179
- - Chromium only survives inside one shell invocation, so launch + check must run together:
180
- `rm -rf /tmp/cp; (setsid /usr/bin/chromium-browser --headless=new --no-sandbox
181
- --user-data-dir=/tmp/cp --remote-debugging-port=9333 --ignore-certificate-errors
182
- about:blank &) ; until curl -s 127.0.0.1:9333/json/version >/dev/null; do sleep 2; done;
183
- BMC_MON_BASE=https://<lan>:8088 node scripts/browser-check.mjs mining`
184
- - Quote `--remote-allow-origins=*` or the shell globs it into filenames.
185
- - **`Page.navigate` to a URL differing only in `#fragment` does not reload the document.**
186
- Reusing a warm browser therefore re-measures whatever was loaded before the last deploy:
187
- `public/**` changes, the page does not, and every reading is of the previous build. The
188
- harness now appends `?t=<ms>` for this reason. Rule 25: prove the browser is running the
189
- bytes you served before believing anything it says about them.
190
- - Do not assert `document.body.firstChild` is an element. Leading whitespace before
191
- `<header>` is a `#text` node in every ordinary document; a probe that checked this
192
- "proved" the Overview rendered nothing when it had rendered fine throughout.
193
- - `Page.loadEventFired` is not reliably seen by the current waiter; the probe's
194
- **load guard** (abort unless `#nav` and body children exist) is what stops a broken
195
- invocation being reported as "the page is blank" — that happened once and the vision
196
- model confidently confirmed a blank white page that was only my own bad URL.
197
- - Vision: this deployment answers with `content: null` and the text in `reasoning`; reading
198
- only `content` silently yields an empty review.
199
- - `mempool-backend.service` has been `failed` since 2026-09-09 16:58 (start-limit hit after
200
- 12 restarts; its own stdout is not in the journal). Unrelated to the above, untouched.