blockyard 0.0.9 → 0.1.1

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 (97) hide show
  1. package/CHANGELOG.md +359 -1
  2. package/README.md +48 -27
  3. package/SECURITY.md +2 -2
  4. package/bin/blockyard.js +2 -1
  5. package/docs/API.md +17 -15
  6. package/docs/ARCHITECTURE.md +128 -10
  7. package/docs/CONFIGURATION.md +39 -30
  8. package/docs/DEFECTS.md +4 -1
  9. package/docs/GETTING-STARTED.md +18 -8
  10. package/docs/INSTALL.md +97 -37
  11. package/docs/MEASUREMENTS.md +147 -0
  12. package/docs/PLAN-SCORCHED-YARD.md +456 -0
  13. package/docs/PLAN-SKIES.md +142 -0
  14. package/docs/SECURITY-AUDIT-2026-09-16.md +647 -0
  15. package/docs/SECURITY.md +58 -22
  16. package/docs/TROUBLESHOOTING.md +44 -5
  17. package/docs/USER-GUIDE.md +505 -35
  18. package/package.json +4 -2
  19. package/public/404.html +1 -1
  20. package/public/css/app.css +393 -82
  21. package/public/donate-qr.png +0 -0
  22. package/public/index.html +353 -109
  23. package/public/js/agents.js +228 -51
  24. package/public/js/app.js +131 -16
  25. package/public/js/blockanoid.js +15 -7
  26. package/public/js/blockout.js +15 -7
  27. package/public/js/blockscene3d.js +230 -38
  28. package/public/js/charts.js +21 -21
  29. package/public/js/depthchart.js +31 -27
  30. package/public/js/details3d.js +1481 -73
  31. package/public/js/doom.js +31 -0
  32. package/public/js/dosaudio.js +48 -0
  33. package/public/js/dosgame.js +389 -0
  34. package/public/js/dosio.js +186 -0
  35. package/public/js/dospc.js +1353 -0
  36. package/public/js/dosworker.js +196 -0
  37. package/public/js/explorer.js +7 -1
  38. package/public/js/livingsky.js +494 -0
  39. package/public/js/login.js +8 -2
  40. package/public/js/markets.js +46 -8
  41. package/public/js/mining.js +314 -36
  42. package/public/js/panels.js +41 -28
  43. package/public/js/pricechart.js +14 -13
  44. package/public/js/quake.js +20 -0
  45. package/public/js/safenext.js +14 -0
  46. package/public/js/scorched.js +1051 -0
  47. package/public/js/scorchedai.js +227 -0
  48. package/public/js/scorchedair.js +286 -0
  49. package/public/js/scorchedfx.js +376 -0
  50. package/public/js/scorchedshop.js +105 -0
  51. package/public/js/scorchedwind.js +69 -0
  52. package/public/js/scorchedyard.js +1338 -0
  53. package/public/js/settings.js +368 -100
  54. package/public/js/soundcard.js +459 -0
  55. package/public/js/tetrust.js +15 -6
  56. package/public/js/tetsound.js +35 -5
  57. package/public/js/theme.js +235 -0
  58. package/public/js/wolf3d.js +22 -0
  59. package/public/js/x86.js +1978 -0
  60. package/scripts/check.js +46 -0
  61. package/scripts/donate-qr.py +12 -9
  62. package/scripts/dos-bench.js +56 -0
  63. package/scripts/index-build.js +9 -2
  64. package/scripts/pool-map.js +152 -36
  65. package/scripts/setup.js +142 -22
  66. package/scripts/shots.mjs +27 -0
  67. package/scripts/smoke.sh +7 -6
  68. package/scripts/tls.js +31 -0
  69. package/scripts/ui.js +4 -2
  70. package/server/auth/sessions.js +33 -13
  71. package/server/chain/blockfile.js +64 -5
  72. package/server/chain/index/build.js +451 -58
  73. package/server/chain/index/heights.js +29 -3
  74. package/server/chain/index/live.js +13 -7
  75. package/server/chain/index/rows.js +6 -1
  76. package/server/chain/index/store.js +28 -5
  77. package/server/chain/index/worker.js +23 -11
  78. package/server/collect/logparse.js +65 -18
  79. package/server/collect/markets.js +76 -7
  80. package/server/collect/mining.js +32 -0
  81. package/server/collect/monitor.js +54 -12
  82. package/server/collect/network.js +305 -0
  83. package/server/config.js +53 -22
  84. package/server/http/api.js +119 -18
  85. package/server/http/games.js +77 -0
  86. package/server/http/server.js +30 -5
  87. package/server/http/sse.js +53 -7
  88. package/server/main.js +66 -11
  89. package/server/rpc/allowlist.js +26 -0
  90. package/server/rpc/client.js +30 -2
  91. package/server/store/audit.js +6 -1
  92. package/server/store/history.js +19 -3
  93. package/server/store/ledger.js +15 -4
  94. package/server/tls/selfsigned.js +160 -0
  95. package/systemd/blockyard.service +41 -8
  96. package/docs/PRIVATE-LEADERBOARD.md +0 -230
  97. package/docs/STATE-2026-09-09.md +0 -200
package/docs/API.md CHANGED
@@ -89,7 +89,7 @@ Other statuses: `204` for `OPTIONS` on any path (`Allow: GET,POST,DELETE,HEAD`).
89
89
 
90
90
  Each route in the table has one of three auth levels:
91
91
 
92
- | Level | Accounts off (default) | Accounts on (`BLOCKYARD_AUTH=1`) |
92
+ | Level | Accounts off (`BLOCKYARD_AUTH=0`) | Accounts on (the default) |
93
93
  |---|---|---|
94
94
  | `none` | Open. | Open. No session needed. |
95
95
  | `any` | Served as the built-in anonymous user (`role: "viewer"`). | Any valid, non-disabled session. Without one: `401`. |
@@ -97,14 +97,14 @@ Each route in the table has one of three auth levels:
97
97
 
98
98
  The roles are `viewer` < `operator` < `admin`. No route in the table requires `viewer` or `operator` by itself. Those two roles matter in two places only: each node action names a minimum role (section 14), and `POST /api/password` needs `admin` to change *another* user's password.
99
99
 
100
- **Open mode (the default, `auth.enabled=false`).** No sign-in. Anyone who can reach the port can read everything (state, charts, events, peers, mempool, explorer, markets, the read-only RPC console) as `viewer`. The ceiling is fixed and nothing can raise it. As a result:
100
+ **Open mode (`auth.enabled=false`, a choice; the default is accounts on).** No sign-in. Anyone who can reach the port can read everything (state, charts, events, peers, mempool, explorer, markets, the read-only RPC console) as `viewer`. The ceiling is fixed and nothing can raise it. As a result:
101
101
 
102
102
  - user administration, `/api/audit` and password changes are closed;
103
103
  - node writes (`/api/action`) are refused unless `BLOCKYARD_ALLOW_WRITES_WITHOUT_AUTH=1` was set deliberately;
104
104
  - `/api/login` answers `403 accounts_disabled`;
105
105
  - `GET /login` redirects (`302`) to `/`.
106
106
 
107
- **Accounts mode (`BLOCKYARD_AUTH=1`).** Sign-in, roles, sessions, CSRF protection and a per-user audit trail. On first start with no users, an `admin` account is created. Its password comes from `BLOCKYARD_ADMIN_PASSWORD`, or is generated and printed once to the server log.
107
+ **Accounts mode (the default).** Sign-in, roles, sessions, CSRF protection and a per-user audit trail. On first start with no users, an `admin` account is created. Its password comes from `BLOCKYARD_ADMIN_PASSWORD`, or is generated and printed once to the server log.
108
108
 
109
109
  ### Session cookie
110
110
 
@@ -224,8 +224,8 @@ Auth `none`, not rate limited. Built for uptime probes. `ok` is `true` when at l
224
224
  {
225
225
  "ok": true,
226
226
  "degraded": [],
227
- "version": "0.0.9",
228
- "build": "0.0.9-fd620adc52",
227
+ "version": "0.1.0",
228
+ "build": "0.1.0-fd620adc52",
229
229
  "scheme": "http",
230
230
  "tls": false,
231
231
  "uptimeSec": 1519,
@@ -242,7 +242,7 @@ Auth `none`, not rate limited. Built for uptime probes. `ok` is `true` when at l
242
242
  What the About page shows: the monitor's version and live build, and the **shape** of the machine it runs on.
243
243
 
244
244
  ```json
245
- { "version": "0.0.9", "build": "0.0.9-a6ecedff3c", "platform": "linux", "release": "7.0.0-31-generic",
245
+ { "version": "0.1.0", "build": "0.1.0-a6ecedff3c", "platform": "linux", "release": "7.0.0-31-generic",
246
246
  "arch": "x64", "cpus": 32, "cpuModel": "AMD Ryzen 9 9950X3D 16-Core Processor",
247
247
  "totalMemGb": 132.3, "node": "v22.23.2", "uptimeSec": 2355 }
248
248
  ```
@@ -261,9 +261,9 @@ Auth `none`. Answers "is the code in my tab the code on disk?" The build id is `
261
261
 
262
262
  ```json
263
263
  {
264
- "version": "0.0.9",
265
- "build": "0.0.9-5223f98d84",
266
- "bootBuild": "0.0.9-fd620adc52",
264
+ "version": "0.1.0",
265
+ "build": "0.1.0-5223f98d84",
266
+ "bootBuild": "0.1.0-fd620adc52",
267
267
  "matchesClient": null,
268
268
  "scheme": "http",
269
269
  "tls": false,
@@ -295,6 +295,7 @@ Top-level keys:
295
295
  ```
296
296
  id label color online chain ibd tip sync progress warnings difficulty hashrateEstEh hashrateNote
297
297
  avgBlockGapSec sizeOnDisk pruned chainwork uptimeSec network mempool peers net attribution blocks
298
+ network (the Mining tab's network row: rewards over 144 blocks, adjustment, halving, adjustments[], hashrate { networkHashPs, series[] }, pools { blocks, luckPct, pools[], filled, todo }; collect/network.js)
298
299
  fees mining utxo chaintxstats indexes tips deployments rpcInfo log health series app user seq
299
300
  ```
300
301
 
@@ -331,7 +332,7 @@ Trimmed example:
331
332
  "health": { "rpc": { "online": true, "...": "..." }, "cadence": { "fast": { "configuredMs": 4000,
332
333
  "effectiveMs": 4000, "stretched": false, "lastRunMs": 120 } }, "quality": [] },
333
334
  "series": { "mempool": { "hour": [ { "t": 1789154820000, "v": 18780 } ], "...": "..." } },
334
- "app": { "version": "0.0.9", "build": "0.0.9-fd620adc52", "scheme": "http", "uptimeSec": 1536,
335
+ "app": { "version": "0.1.0", "build": "0.1.0-fd620adc52", "scheme": "http", "uptimeSec": 1536,
335
336
  "sseClients": 5, "self": { "rssMb": 175.6, "heapMb": 42.8, "cpuPct": 2.71 }, "serverTime": 1789155688322 },
336
337
  "user": { "username": "anonymous", "role": "viewer", "id": "anonymous", "disabled": false, "lastLoginAt": null },
337
338
  "seq": 406
@@ -1029,8 +1030,9 @@ To poll incrementally, keep `maxSeq` and pass it back as `since`. The filters ar
1029
1030
 
1030
1031
  Exchange prices from the public REST APIs of Coinbase, Kraken, Bitstamp, Bitfinex and OKX. **This is the only outbound connection BlockYard makes that is not to the node.** It runs server-side, because the page's CSP allows `connect-src 'self'` only.
1031
1032
 
1032
- - **On demand.** Nothing is fetched until someone calls `/api/markets` or `/api/markets/depth`. Each call "touches" the feed. The dashboard's Overview makes that call by default (Display settings Markets & Price Price line on Overview), so a monitor with anyone on its landing page is polling. While touched, it polls tickers every 15 s, hourly candles every 5 min and order books every 30 s. It stops 10 minutes after the last touch.
1033
- - `BLOCKYARD_MARKETS=0` (or `markets.enabled=false`) turns it off. Both endpoints then answer `{ "ok": true, "enabled": false, "note": "market data is off on this monitor (...)" }`.
1033
+ - **Off by default.** Polling runs only while the Display setting **Markets & Price Enable market polling** is on (`markets.polling` in the shared settings file, read per request). With it off, both endpoints answer `{ "ok": true, "enabled": false, "polling": false, "note": "market polling is off ..." }`, the call parks the feed, and the monitor makes no outbound connection but to the node.
1034
+ - `BLOCKYARD_MARKETS=0` (or `markets.enabled=false`) removes the feed altogether; both endpoints then answer `{ "ok": true, "enabled": false, "note": "market data is off on this server ..." }` whatever the setting.
1035
+ - **On demand.** With polling on, nothing is fetched until someone calls `/api/markets` or `/api/markets/depth`. Each call "touches" the feed. The dashboard's Overview makes that call by default (Display settings → Markets & Price → Price line on Overview), so a monitor with anyone on its landing page is polling. While touched, it polls tickers every 15 s, hourly candles every 5 min and order books every 30 s. It stops 10 minutes after the last touch.
1034
1036
  - An exchange that fails keeps its last data and reports `error`. It is never dropped or zero-filled.
1035
1037
 
1036
1038
  ### `GET /api/markets`
@@ -1153,7 +1155,7 @@ The default is **deny**. The rules are applied in this order, and the first matc
1153
1155
  Consequences worth knowing:
1154
1156
 
1155
1157
  - The list is fixed in code, and no setting widens it.
1156
- - `get*` and `list*` wallet reads (for example `getbalance`, `listunspent`) are allowed if the node has a wallet loaded.
1158
+ - Wallet RPCs are refused by name, reads included (`getbalance`, `listunspent`, `listdescriptors`, `gethdkeys`, …): some of them return private keys, and the monitor has no use for a wallet.
1157
1159
  - `createrawtransaction` is allowed: it only builds an unsigned transaction and changes nothing.
1158
1160
  - `/api/config` → `allowlist` publishes a summary: the prefixes, the size of the deny list and the default decision.
1159
1161
 
@@ -1222,7 +1224,7 @@ If the node refuses, the answer is `200` with `{ "ok": false, "action", "method"
1222
1224
 
1223
1225
  ## 15. Accounts, sessions, users and audit
1224
1226
 
1225
- These routes are meaningful only with `BLOCKYARD_AUTH=1`. In open mode they answer as noted.
1227
+ These routes are meaningful only with accounts on (the default). In open mode they answer as noted.
1226
1228
 
1227
1229
  The user object returned by these routes (`publicUser`):
1228
1230
 
@@ -1378,7 +1380,7 @@ The monitor's own health: process, RPC client, log tail, stream clients and the
1378
1380
  ```json
1379
1381
  {
1380
1382
  "self": { "t": 1789155700000, "rssMb": 175.6, "heapMb": 42.8, "sseClients": 5, "usersActive": 0,
1381
- "cpuPct": 2.71, "eventRate": 0, "build": "0.0.9-fd620adc52" },
1383
+ "cpuPct": 2.71, "eventRate": 0, "build": "0.1.0-fd620adc52" },
1382
1384
  "nodes": [
1383
1385
  { "id": "main",
1384
1386
  "rpc": { "nodeId": "main", "url": "http://127.0.0.1:8332", "cookieSource": "...", "online": true,
@@ -192,8 +192,9 @@ it directly; running the file as a script calls it and prints the banner.
192
192
  missing is skipped with a logged reason rather than kept as a permanently
193
193
  offline panel. `wireMonitor()` connects each monitor's events to the SSE hub
194
194
  and coalesces pushes to one snapshot per second.
195
- 8. `MarketFeed` is created if `markets.enabled` is set. It stays idle until the
196
- Markets API is requested.
195
+ 8. `MarketFeed` is created if `markets.enabled` is set. It stays idle until the Markets
196
+ API is requested with the **Enable market polling** setting on (off by default; read from
197
+ the shared settings file per request, `marketsPollingOn` in `http/api.js`).
197
198
  9. One HTTP(S) server is created per bound address, all sharing the same `app`.
198
199
  An address missing at boot is skipped with a warning. Boot is fatal only when
199
200
  none of the configured addresses exist.
@@ -211,8 +212,8 @@ it directly; running the file as a script calls it and prints the banner.
211
212
  `address-index-building` quality flag, `index` events at start, finish and
212
213
  failure, and the follower started on completion (section 2.8).
213
214
  `app.shutdown()` stops timers, closes streams, stops monitors, saves history
214
- and sessions, and closes the listeners. A build in flight is not resumed:
215
- the next start begins it again.
215
+ and sessions, and closes the listeners. A build in flight stops with the
216
+ process and resumes on the next start from its journal (section 2.8).
216
217
 
217
218
  `boot({ log })` and `loadConfig({ ifaces, now })` are **seams**: tests inject a
218
219
  logger, a fake interface list, or a fake clock instead of intercepting
@@ -346,7 +347,9 @@ Bitstamp, Bitfinex and OKX.
346
347
  with its error, never zero-filled. Tickers older than three intervals are marked
347
348
  `stale`.
348
349
 
349
- `BLOCKYARD_MARKETS=0` (or `markets.enabled: false`) disables all of it.
350
+ Polling is off by default the **Enable market polling** Display setting turns it on, and a
351
+ request with it off parks the feed. `BLOCKYARD_MARKETS=0` (or `markets.enabled: false`)
352
+ removes the feed altogether.
350
353
 
351
354
  ### 2.4 The RPC lane (`server/rpc/client.js`)
352
355
 
@@ -523,11 +526,12 @@ flowchart LR
523
526
 
524
527
  ### 2.7 Auth (`server/auth/*`)
525
528
 
526
- - **Open by default.** With `auth.enabled: false` (the default), every request is
529
+ - **Sign-in by default** (since 2026-09-15; it shipped open in 0.0.9), bound to
530
+ `127.0.0.1`. With `auth.enabled: false` (`BLOCKYARD_AUTH=0`), every request is
527
531
  served as a frozen anonymous user with role `viewer`. That ceiling is hardcoded,
528
532
  not configurable. Admin routes return 403 in both modes, CSRF is not needed
529
533
  because there is no cookie to ride, and rate limits apply per client address.
530
- - **Accounts.** `BLOCKYARD_AUTH=1` turns on:
534
+ - **Accounts** (the default; `BLOCKYARD_AUTH=1` restores them after an override):
531
535
  - scrypt password hashes (upgraded to current parameters on login)
532
536
  - session tokens that are stored hashed, with idle and absolute TTLs
533
537
  - double-submit CSRF (the token in a header or body is compared with the
@@ -573,7 +577,24 @@ files. The numbers are in `docs/MEASUREMENTS.md` §28-30 and the history in
573
577
  bucket files; a check that every height is indexed exactly once, or the build
574
578
  stops rather than publish a hole; each bucket sorted into `seg-XX.rows` plus a
575
579
  sparse `seg-XX.idx` (one key per 4,096 rows); and a manifest written last, so an
576
- index without one is unfinished. Measured on the whole chain: 29 min 45 s on 16
580
+ index without one is unfinished. **An interrupted build resumes** from
581
+ `build-journal.json`, written beside the output with a temporary name, fsync and
582
+ rename, and carrying a SHA-256 of its own body. The scan resumes per block file:
583
+ a checkpoint (at most once a minute, and when the scan ends or fails in an orderly
584
+ way) fsyncs the buckets written since the last one and records the files
585
+ finished, each bucket's length and running CRC-32, the heights seen and the
586
+ counters; a resume cuts every bucket back to its journaled length, so rows from a
587
+ half-appended file are gone, and scans that file again. The sort resumes per
588
+ bucket: the worker checks the input against the journaled length and CRC, writes
589
+ the segment with fsync and rename, the journal records it, and only then is the
590
+ input removed. The journal names the journal version, index format, row and
591
+ block sizes, chain, tip height and hash and the `--files` selection; a resume
592
+ works to the journal's tip, not the node's newer one, so the result is byte for
593
+ byte the index an uninterrupted build would have written. A journal that is torn,
594
+ of another format or selection, whose tip hash the node's active chain no longer
595
+ has, or whose files on disk disagree with it is discarded with the reason logged,
596
+ and the build starts over. The manifest is still last; the journal is removed
597
+ after it. Measured on the whole chain: 29 min 45 s on 16
577
598
  workers, 5.89 billion rows, 123.7 GB (§30). The build reads ~880 GB and writes
578
599
  ~120 GB, so `--out` should be a different device from the block files.
579
600
  - **Lookups** (`chain/index/store.js`, `IndexStore`). The sparse keys of the 256
@@ -603,8 +624,8 @@ files. The numbers are in `docs/MEASUREMENTS.md` §28-30 and the history in
603
624
  the phase's own rate, `paused while the node's RPC is slow`), refreshed at most
604
625
  every 5 s; `index` events mark start, finish and failure; on finish the follower
605
626
  starts and address pages go live with no restart; failure leaves
606
- `address-index-build-failed` naming the command to run by hand. There is no
607
- resume: a build stopped with the server starts over next time.
627
+ `address-index-build-failed` naming the command to run by hand. A build stopped with the server resumes next time; the flag's ETA is
628
+ computed from what this run has done (`from` in the progress), not from zero.
608
629
  - **Following the chain** (`chain/index/live.js`, `LiveIndex`). The base is
609
630
  immutable and covers the chain to the block it was built at. The follower polls
610
631
  the node's tip, rolls the tail back to the fork if a block it holds is no longer
@@ -659,6 +680,7 @@ loaded from the same origin. There is no build step and no framework.
659
680
  | `settings.js` | display settings: `DEFAULTS`, the `PANEL` rows of the settings dialog, `normalise()`, and the option builders (`spaceOptions`, `enabledEffects`, ...) the boards read; stored on the server (`/api/settings`) with a `localStorage` copy |
660
681
  | `about.js` | the About page (version, system and node info) |
661
682
  | `tetris.js` / `tetrust.js`, `breakout.js` / `blockout.js`, `arkanoid.js` / `blockanoid.js`, `tetsound.js` | the Diversions: pure game rules in the first file of each pair, the tab drawn on the 3D engine in the second, and Tetrust's sound |
683
+ | `x86.js`, `dospc.js`, `soundcard.js`, `dosworker.js`, `dosaudio.js`, `dosio.js`, `dosgame.js`, `doom.js`, `quake.js` | the DOS Diversions: an i386 interpreter, the PC around it (DOS/4GW for DOOM, the go32 stub and CWSDPMI for Quake), a Sound Blaster Pro 2 with an OPL3, the worker the machine runs in, the AudioWorklet it plays through, the pure keyboard/config/text-mode helpers, the shared tab, and each game's own few lines (section 3.4) |
662
684
  | `fmt.js` | formatters: decimal units (as the node prints them), `–` for anything absent |
663
685
  | `login.js` | the login page (a separate file because of the CSP) |
664
686
 
@@ -790,6 +812,102 @@ What this means for front-end code:
790
812
 
791
813
  ---
792
814
 
815
+ ### 3.4 The DOS Diversions: Wolfenstein 3D, DOOM and Quake
816
+
817
+ The shareware `WOLF3D.EXE` v1.4, `DOOM.EXE` v1.9 and `QUAKE.EXE` v1.06 run unmodified on a PC emulated in
818
+ the browser.
819
+ Nothing is ported and no dependency is used; every layer is this repository's own:
820
+
821
+ ```
822
+ wolf3d.js / doom.js / quake.js -> dosgame.js (page) --scancodes, mouse, run/pause--> dosworker.js (Worker)
823
+ ^ |
824
+ | frames (320x200 indices + palette), text-mode cells, stats | createPC() dospc.js
825
+ +----------------------------------------------------------------------+ createCpu() x86.js
826
+ dosaudio.js (AudioWorklet) <--stereo PCM over a MessagePort-----------------+ soundcard.js (SB Pro 2 + OPL3)
827
+ ```
828
+
829
+ - **`x86.js`** is a user-mode i386 with an x87, interpreted. No paging or rings: a DOS extender's
830
+ program runs in protected mode, and segment registers carry a base and a size. EIP is
831
+ kept linear and converted at the edges (a pushed return address, a loaded jump target) against
832
+ the code segment's base, ESP is an offset in SS: DOS/4GW's segments are all at 0, DJGPP's at the
833
+ program's memory block. A code segment whose descriptor is 16-bit decodes 16-bit (DJGPP's
834
+ start-up and exit run small 16-bit helpers in DOS memory). **Real mode** is a switch
835
+ (`cpu.realMode`) for Wolfenstein 3D: a segment's base is its value times sixteen, code is 16-bit,
836
+ the stack pointer is SP and wraps inside its segment, the string instructions count with SI, DI
837
+ and CX, and an interrupt pushes a 16-bit frame and goes through the vector table at 0:0. Real-mode
838
+ code runs through `step()` rather than the decoded cache: it needs about a third of the speed it
839
+ gets. The page's CSP forbids eval, so there
840
+ is no JIT; speed comes from keeping every value an int32 (a `>>> 0` above 2^31 is a double and,
841
+ in a closure variable, an allocation), lazy flags recorded in an `Int32Array`, one try/catch
842
+ around the loop rather than each instruction, and 32-bit fast paths for the instructions
843
+ compilers emit most. The FPU keeps its stack in a Float64Array and converts operands through
844
+ typed-array views. **Decoded instructions are cached**: each is decoded once into three int32s
845
+ (a handler number with its registers packed beside it, a displacement, an immediate) in an
846
+ Int32Array per 4 KB page, and `run()` dispatches on the handler number with one switch; the forms
847
+ it does not specialise run through `step()`. Not closures -- a closure per instruction made every
848
+ call megamorphic and ran slower than the interpreter. Writes into a page holding decoded code
849
+ clear only the instructions they overlap (DOOM and Quake both patch constants into their own span
850
+ drawers on every call), and the machine calls `cpu.invalidate()` after it writes memory directly.
851
+ The ALU operations have a handler each (their flag bookkeeping inline, not one shared routine
852
+ branching on the operation), so do the x87 forms Quake runs most, and the loop forms a memory
853
+ operand's address inline before the switch: the switch is too big for V8 to inline helpers into
854
+ every case, and those calls were most of what was left. About 141 million instructions a second on
855
+ Quake headless on one core and 115-118 in a Chromium worker; Quake's timedemo 52.5 fps of wall time
856
+ (MEASUREMENTS §32-35).
857
+ - **`dospc.js`** is the machine, and it plays whichever DOS extender the program was bound to.
858
+ `boot()` tells them apart by the file: an EXE with neither an LE nor a COFF image inside is a
859
+ plain DOS program, and `bootMZ` loads it as DOS did -- a PSP, the image after it with its segment
860
+ relocations applied, the rest of conventional memory its block, the vector table filled with
861
+ pointers at stubs the machine answers (a program that installs its own handler gets it called),
862
+ and DOS's allocator behind INT 21h 48h/49h/4Ah, which Wolfenstein 3D uses to shrink itself and
863
+ claim the rest. `loadLE` finds DOOM's LE executable inside the DOS/4GW
864
+ stub, loads it at +1 MB and applies its fixups; `parseCoff`/`bootCoff` find Quake's COFF image
865
+ behind the go32 stub and do what that stub leaves behind -- a memory block with the sections in
866
+ it, selectors based at it, a transfer buffer with the PSP right below it (DJGPP's libc finds the
867
+ PSP by subtracting 100h), and the "stubinfo" crt0 reads through FS. The extenders themselves never
868
+ run; this file answers INT 21h (files from an in-memory, case-insensitive directory tree, written
869
+ files handed to the host on close, and a real system file table, because DJGPP's `fstat` walks
870
+ it), INT 31h DPMI (descriptors with base and size, memory blocks, protected-mode vectors,
871
+ simulated real-mode interrupts), INT 10h/16h/33h, and the hardware a DOS game programs directly:
872
+ the 8259s, the 8254 (with the BIOS tick count kept in step with it, which DJGPP's `uclock` reads),
873
+ the keyboard controller, and a VGA with planar memory, unchained mode, write mode 1's latched copy
874
+ (Wolfenstein 3D and DOOM copy between pages with it) and CRTC page flipping (how DOOM and
875
+ Wolfenstein 3D draw) as well as the linear window (how Quake does). The clock is injected (`now()`): wall
876
+ time in the worker, instruction count in tests, so a headless boot is the same run every time.
877
+ - **`soundcard.js`** is a Sound Blaster Pro 2 at 220h/IRQ 7/DMA 1 — the DSP's command set and the
878
+ 8237 DMA controller it pulls samples through — and an OPL3 modelled as operators with
879
+ documented envelope rates. `tick(t)` produces output for the machine time that passed and raises
880
+ the end-of-block interrupt from inside the same loop.
881
+ - **`dosworker.js`** takes a game's name, fetches its files (`dosio.js` `GAMES`), and runs the
882
+ machine in ~10 ms slices, yielding between them so input arrives. It sends a frame when the CRTC
883
+ start address or the palette changed, or once the linear window has been written and a slice has
884
+ passed without more writes (never half of Quake's copy). The frame buffer bounces between the
885
+ worker and the page so no frame allocates. Savegames and configs are kept in IndexedDB, one
886
+ database a game.
887
+ - **`dosgame.js`** is a game's tab: it draws, captures input, pauses when the tab is not on screen
888
+ (the worker's clock stops, so nothing moves), and uses a ScriptProcessor when `audioWorklet` is
889
+ unavailable — a plain-HTTP LAN address is not a secure context. `doom.js` and `quake.js` give it
890
+ names, key lists and switches, and so does `wolf3d.js`.
891
+
892
+ The game files are served from `games/<game>_dos/` by `server/http/games.js`
893
+ (`/games/<game>/<path>`: a game it names, at most one directory and 8.3 names of `.EXE`, `.WAD`,
894
+ `.PAK`, `.CFG` and `.WL1`, behind the session when accounts are on) rather than from `public/`, whose every
895
+ file feeds the build id.
896
+
897
+ **Provenance** (2026-09-16, audit I6). The files are the three shareware releases, kept byte for byte
898
+ (`.gitattributes` marks `games/**` binary so git never rewrites them), and are served to the browser,
899
+ never executed on the server:
900
+
901
+ | Directory | Release | Its own terms, as shipped |
902
+ |---|---|---|
903
+ | `games/wolf3d_dos/` | Wolfenstein 3D v1.4 shareware (Apogee Software, 1992) | none beyond `file_id.diz`: the release's shareware licence text is not bundled |
904
+ | `games/doom_dos/` | DOOM v1.9 shareware (id Software; `DOOM1.WAD`) | `README.TXT`, `ORDER.FRM`, the DOOM FAQ v6.666 |
905
+ | `games/quake_dos/` | Quake v1.06 shareware (id Software, 1 October 1996) | `LICINFO.TXT`, `SLICNSE.TXT`, `READV106.TXT` |
906
+
907
+ `games/SHA256SUMS` lists the SHA-256 of every tracked file under `games/` (`cd games && sha256sum -c
908
+ SHA256SUMS`), and `test/audit-2026-09-16-low-scripts.test.js` recomputes them, so a changed game file
909
+ is a failing test rather than an unnoticed one.
910
+
793
911
  ## 4. The 3D engine
794
912
 
795
913
  The 3D viewer turns a set of transactions, or any caller-supplied tiles, into
@@ -95,18 +95,20 @@ Durations are in milliseconds unless the name says otherwise (`retentionHours`).
95
95
 
96
96
  | key | default | meaning |
97
97
  |---|---|---|
98
- | `server.host` | `"0.0.0.0"` | Address to listen on. Kept for compatibility; `server.hosts` wins when both are set. |
98
+ | `server.host` | `"127.0.0.1"` | Address to listen on — this machine only, by default. Kept for compatibility; `server.hosts` wins when both are set. |
99
99
  | `server.hosts` | *(unset; falls back to `host`)* | Addresses to listen on: an array (`["192.0.2.10", "2001:db8::10"]`), or a single string with commas (`"192.0.2.10,198.51.100.7"`). One HTTP server is started per address, and all of them share sessions, rate limits and monitors. Entries must be address literals: IPv4, IPv6, `0.0.0.0`, `::`, or `localhost`. Hostnames are refused. See [Binding](#binding-to-specific-addresses). |
100
100
  | `server.port` | `21000` | TCP port. It is the same port on every address. It must be an integer from 1 to 65535. |
101
101
  | `server.allowCidrs` | `[]` | Client allowlist. Empty means every client that can reach the port is admitted. Otherwise only addresses inside one of the networks connect, and everyone else gets HTTP 403 (the reason goes to the server log). Entries are CIDRs or bare addresses (a bare address means `/32` or `/128`), IPv4 or IPv6, compared bit by bit. An entry that cannot be parsed stops the boot. |
102
102
  | `server.trustProxy` | `false` | When `true`, the client address is the **first** entry of the `X-Forwarded-For` header instead of the socket's peer address. That address feeds the CIDR allowlist, the rate limits and the audit log. Turn it on only when a reverse proxy you control is the sole way in and it sets that header. Otherwise any client can pick its own address and walk past `allowCidrs`. |
103
- | `server.tls.cert` | `null` | Path to a PEM certificate. TLS is on only when `cert` and `key` are both set, and then every listener serves HTTPS. |
103
+ | `server.tls.enabled` | `true` | HTTPS on every listener. With no `cert`/`key` named, the server makes its own self-signed certificate under `<data>/tls/` on first start (see INSTALL §9). `false` serves plain HTTP, for a reverse proxy that terminates TLS. |
104
+ | `server.tls.cert` | `null` | Path to a PEM certificate of your own, used instead of the made one. Set it together with `key`. |
104
105
  | `server.tls.key` | `null` | Path to the PEM private key for `cert`. This file is secret. |
105
106
  | `server.tls.hstsMs` | `172800000` (2 days) | `max-age` of the `Strict-Transport-Security` header. It is sent on TLS responses only. `0` turns it off. `includeSubDomains` and `preload` are never sent. |
106
107
 
107
108
  When TLS is on, `auth.secureCookie` is forced to `true`. At startup the server logs
108
109
  the certificate's SHA-256 fingerprint and whether it is self-signed. It also warns
109
- when the certificate expires within 14 days.
110
+ when the certificate expires within 14 days — and remakes its own, if it made it, at that
111
+ point or when the certificate no longer names a bound address.
110
112
 
111
113
  ### nodes
112
114
 
@@ -127,7 +129,7 @@ node gets its own charts and event stream.
127
129
  | `logFile` | none | The node's log file, tailed only when `log.enabled` is `true`. With the log source off (the default) this field is ignored, and a line in the startup log says so. |
128
130
  | `logStaleMs` | *(uses `log.staleMs`)* | Per-node override for how long the log may go without new bytes before the monitor reports it as silent. Use a smaller value for a node that is known to log often. |
129
131
  | `addressIndex` | none | Directory of the address index, built from this node's own block files (`<datadir>/blocks`). With it, the explorer's address page shows history, balance, unspent outputs and per-transaction amounts; without it, Core cannot answer those and the page says so. If the directory holds no finished index (no `manifest.json`) when the server starts, **the server builds one in the background** (see the next two keys). One index serves every node on the same chain. The server keeps it current as blocks arrive (a follower per directory writes `live.log` and `layers/` inside it, so the directory must be writable by the service); the page says if it is behind or has stopped following. See `docs/MEASUREMENTS.md` §30. |
130
- | `addressIndexBuild` | *(unset)* | `"manual"` keeps the server from building a missing address index on start. Otherwise a missing index is built inside the server on worker threads while every page keeps serving: the progress is the `address-index-building` quality flag on the Overview (phase, done of total, rows, time left, and whether it is paused), an event of kind `index` marks the start, the finish and a failure (the browser shows each as a notification), the follower starts the moment the build finishes so address pages go live with no restart, and a failure raises `address-index-build-failed` with the command to run by hand. The build is paced by the node's own RPC: it holds while the node is failing, its breaker is open or its average latency is above `rpc.slowLatencyMs`, and eases off above 40% of it. A build interrupted by a stop does not resume; the next start begins it again. `npm run setup --build-later` writes this key. |
132
+ | `addressIndexBuild` | *(unset)* | `"manual"` keeps the server from building a missing address index on start. Otherwise a missing index is built inside the server on worker threads while every page keeps serving: the progress is the `address-index-building` quality flag on the Overview (phase, done of total, rows, time left, and whether it is paused), an event of kind `index` marks the start, the finish and a failure (the browser shows each as a notification), the follower starts the moment the build finishes so address pages go live with no restart, and a failure raises `address-index-build-failed` with the command to run by hand. The build is paced by the node's own RPC: it holds while the node is failing, its breaker is open or its average latency is above `rpc.slowLatencyMs`, and eases off above 40% of it. A build interrupted by a stop resumes on the next start: a journal in the index directory records the block files already scanned and the buckets already sorted, so only the work it cannot prove finished is done again (at most about a minute of scanning), and the log says where it resumed. A journal for another tip that the node no longer has, another index format, or one that is damaged is thrown away with the reason in the log, and the build starts over. `npm run setup --build-later` writes this key. |
131
133
  | `addressIndexWorkers` | half of a dedicated build's count, at most 4 | Worker threads for the background build. A dedicated build (`scripts/index-build.js`) uses `cpus − 4`, one per ~2.5 GB of memory, at most 16; the server takes half of that, at most 4, because the node shares the disk. **Use 1 on a spinning disk**: parallel readers seek against each other and against the node. The installer writes the number you give it. |
132
134
  | `optional` | `false` | Marks a node whose absence is expected, such as a test or benchmark node. Its failures are logged at a lower severity, a missing datadir is reported at `info` instead of `warn`, and it does not count as a required node in `/api/health`. |
133
135
  | `color` | `"#f7931a"` | Accent colour for this node in the UI. |
@@ -250,7 +252,7 @@ sessions, CSRF protection and a per-user audit trail.
250
252
 
251
253
  | key | default | meaning |
252
254
  |---|---|---|
253
- | `auth.enabled` | `false` | Turn accounts on. |
255
+ | `auth.enabled` | `true` | Accounts. `false` is open mode: anyone who can reach the port reads the monitor as a viewer. |
254
256
  | `auth.dataDir` | same as `store.dir` | Where `users.json` and `sessions.json` live. |
255
257
  | `auth.sessionTtlMs` | `259200000` (72 h) | Absolute session lifetime, counted from sign-in. |
256
258
  | `auth.idleTtlMs` | `28800000` (8 h) | A session unused for this long expires. Until 2026-09-13 the two defaults were the other way round, so the idle check could never fire and a session was 8 h whatever you did. |
@@ -262,6 +264,7 @@ sessions, CSRF protection and a per-user audit trail.
262
264
  | `auth.loginMaxAttempts` | `8` | Intended: failed logins per username before lockout. See [Known quirks](#known-quirks); the effective value is 8 whatever you set. |
263
265
  | `auth.loginWindowMs` | `300000` (5 min) | Intended: window in which failed attempts are counted. See [Known quirks](#known-quirks); the effective value is 5 minutes. |
264
266
  | `auth.lockoutMs` | `600000` (10 min) | How long a username stays locked after too many failures. |
267
+ | `auth.openNodeConfigFromNetwork` | `false` | With accounts off, the node connection form (test and save) answers only a caller on this machine's loopback address, and never behind `server.trustProxy`. A browser check cannot stop a script, and a script that can save the connection decides where the node's cookie is sent after the next restart. `true` lets any client that reaches the port use the form. With accounts on, the form needs an admin either way. |
265
268
  | `auth.cookieName` | `"blockyard_sid"` | Name of the session cookie. |
266
269
  | `auth.secureCookie` | `false` | Mark the session cookie `Secure`. Forced to `true` when `server.tls` is on. Set it yourself only when a TLS-terminating reverse proxy sits in front, because browsers never send a `Secure` cookie over plain HTTP. |
267
270
 
@@ -340,17 +343,18 @@ Available actions:
340
343
  The Markets tab fetches public BTC/USD prices, hourly candles and order books from
341
344
  five exchanges over HTTPS: Coinbase, Kraken, Bitstamp, Bitfinex and OKX (OKX quotes
342
345
  BTC/USDT). The fetches run on the server, not in the browser. This is BlockYard's
343
- only outbound connection other than the node. Exchanges see this server's IP
344
- address and a User-Agent, nothing about the node. Polling starts when a browser asks
345
- for market data -- the Markets and Kiosk tabs, and Overview's price line, which is on by
346
- default (Display settings Markets & Price Price line on Overview) -- and stops
347
- `idleAfterMs` after the last request, so an unwatched monitor makes no exchange traffic.
348
- With that line switched off, only Markets and Kiosk start it. The exchange list is fixed
349
- in code.
346
+ only outbound connection other than the node, and **polling is off by default**: a fresh
347
+ install makes no request to anyone but the node until someone ticks **Display settings → Markets & Price → Enable market polling** in the browser (a Display
348
+ setting, stored in `config/blockyard.json` and shared by every screen; no restart).
349
+ Exchanges then see this server's IP address and a User-Agent, nothing about the node. Polling starts when a browser asks for market data
350
+ -- the Markets and Kiosk tabs, and Overview's price line (Display settings Markets &
351
+ Price Price line on Overview) -- and stops `idleAfterMs` after the last request, so an
352
+ unwatched monitor makes no exchange traffic. With that line switched off, only Markets
353
+ and Kiosk start it. The exchange list is fixed in code.
350
354
 
351
355
  | key | default | meaning |
352
356
  |---|---|---|
353
- | `markets.enabled` | `true` | `false` removes the feed entirely, and no request is ever made. |
357
+ | `markets.enabled` | `true` | `false` removes the feed entirely: no request is ever made, Markets and Kiosk say so, and the **Enable market polling** checkbox cannot turn it on. |
354
358
  | `markets.tickerMs` | `15000` | Ticker (last, bid, ask, 24 h volume) refresh. |
355
359
  | `markets.candleMs` | `300000` | Hourly candle refresh. |
356
360
  | `markets.bookMs` | `30000` | Order book refresh, for the depth chart. |
@@ -378,12 +382,13 @@ Environment variables override `config/local.json`.
378
382
  | variable | sets | type | default | meaning |
379
383
  |---|---|---|---|---|
380
384
  | `BLOCKYARD_CONFIG` | *(which file is read)* | path or `none` | `<repo>/config/local.json` | Configuration file to read. `none`/`off`/`no`/`-` reads no file. |
381
- | `BLOCKYARD_BIND` | `server.host` | list | `0.0.0.0` | Listen address(es), for example `127.0.0.1` or `192.0.2.10,2001:db8::10`. If both are set, this wins over `BLOCKYARD_HOST`. Ignored when the file sets `server.hosts` (see [Known quirks](#known-quirks)). |
382
- | `BLOCKYARD_HOST` | `server.host` | list | `0.0.0.0` | Same as `BLOCKYARD_BIND`. |
385
+ | `BLOCKYARD_BIND` | `server.host` | list | `127.0.0.1` | Listen address(es), for example `0.0.0.0` or `192.0.2.10,2001:db8::10`. If both are set, this wins over `BLOCKYARD_HOST`. Ignored when the file sets `server.hosts` (see [Known quirks](#known-quirks)). |
386
+ | `BLOCKYARD_HOST` | `server.host` | list | `127.0.0.1` | Same as `BLOCKYARD_BIND`. |
383
387
  | `BLOCKYARD_PORT` | `server.port` | number | `21000` | Listen port. |
384
388
  | `BLOCKYARD_ALLOW_CIDRS` | `server.allowCidrs` | list | *(empty: everyone)* | Client allowlist, for example `192.0.2.0/24,2001:db8::/32`. |
385
389
  | `BLOCKYARD_TRUST_PROXY` | `server.trustProxy` | boolean | `false` | Take the client address from `X-Forwarded-For`. |
386
- | `BLOCKYARD_TLS_CERT` | `server.tls.cert` | path | unset | PEM certificate. Set it together with `BLOCKYARD_TLS_KEY`. |
390
+ | `BLOCKYARD_TLS` | `server.tls.enabled` | boolean | `true` | `0` serves plain HTTP (behind a TLS-terminating proxy). |
391
+ | `BLOCKYARD_TLS_CERT` | `server.tls.cert` | path | unset | PEM certificate of your own, instead of the made one. Set it together with `BLOCKYARD_TLS_KEY`. |
387
392
  | `BLOCKYARD_TLS_KEY` | `server.tls.key` | path | unset | PEM private key. Set it together with `BLOCKYARD_TLS_CERT`. |
388
393
  | `BLOCKYARD_NODE_URL` | `nodes[0].rpcUrl` | URL | `http://127.0.0.1:8332` | RPC endpoint of the first node. |
389
394
  | `BLOCKYARD_DATADIR` | `nodes[0].datadir` | path | `/home/bitcoin/.bitcoin` | Data directory of the first node. It also **clears** `nodes[0].cookieFile`, so the cookie is looked up under the new datadir. |
@@ -396,7 +401,8 @@ Environment variables override `config/local.json`.
396
401
  | `BLOCKYARD_RPC_STALE_DROP` | `rpc.staleDropMs` | number | `12000` | Drop poll answers older than this. |
397
402
  | `BLOCKYARD_DATA` | `store.dir` | path | `<repo>/data` | Data directory. Also the default `auth.dataDir`. |
398
403
  | `BLOCKYARD_RETENTION_HOURS` | `store.retentionHours` | number | `72` | Chart history retention. |
399
- | `BLOCKYARD_AUTH` | `auth.enabled` | boolean | `false` | Turn accounts on. |
404
+ | `BLOCKYARD_AUTH` | `auth.enabled` | boolean | `true` | Accounts; `0` is open mode. |
405
+ | `BLOCKYARD_OPEN_NODE_CONFIG_FROM_NETWORK` | `auth.openNodeConfigFromNetwork` | boolean | `false` | In open mode, let any client use the node connection form, not only this machine. |
400
406
  | `BLOCKYARD_SECURE_COOKIE` | `auth.secureCookie` | boolean | `false` | `Secure` session cookie. Use it behind a TLS terminator; it is automatic with built-in TLS. |
401
407
  | `BLOCKYARD_ADMIN_PASSWORD` | *(none)* | string | *(generated)* | Password for the `admin` account created on first boot when accounts are on and no users exist. It is ignored once any user exists. It is a secret, so do not leave it in a unit file after first boot. |
402
408
  | `BLOCKYARD_ENABLE_ACTIONS` | `actions.enabled` | boolean | `false` | Master switch for node writes. |
@@ -404,7 +410,7 @@ Environment variables override `config/local.json`.
404
410
  | `BLOCKYARD_ALLOW_WRITES_WITHOUT_AUTH` | `actions.allowWritesWithoutAuth` | boolean | `false` | Permit actions while accounts are off. |
405
411
  | `BLOCKYARD_LOG_SOURCE` | `log.enabled` | boolean | `false` | `1` tails node log files; `0` (or unset) runs on RPC alone. |
406
412
  | `BLOCKYARD_LOG_LEVEL` | `log.level` | string | `info` | `debug`, `info`, `warn` or `error`. |
407
- | `BLOCKYARD_MARKETS` | `markets.enabled` | boolean | `true` | `0` turns the Markets feed off. |
413
+ | `BLOCKYARD_MARKETS` | `markets.enabled` | boolean | `true` | `0` removes the Markets feed; the polling checkbox then cannot turn it on. |
408
414
  | `BLOCKYARD_MINING` | *(none)* | `0` or anything | on | `0` turns off miner attribution, which decodes each block's coinbase to show the pool tag. Block sizes, fees and weights are unaffected. Only the literal `0` disables it. |
409
415
  | `BLOCKYARD_MINING_BACKFILL` | *(none)* | number | `36` | How many recent blocks are attributed to miners at startup. |
410
416
  | `BLOCKYARD_MINING_TEMPLATE` | *(none)* | `0` or anything | on | `0` turns off the "block being built" card. Since 2026-09-13 the template is **assembled from the mempool this monitor already reads**, so leaving it on costs your node no RPC call at all — it costs this process ~50-70 ms of CPU per assembly. Before that it was a `getblocktemplate` worth over a second of the node's single RPC thread, which is why the switch exists. Only the literal `0` disables it. |
@@ -577,10 +583,10 @@ instead:
577
583
  BLOCKYARD_ADMIN_PASSWORD='choose-a-long-passphrase' npm start
578
584
  ```
579
585
 
580
- Then create personal accounts with `npm run user -- create <name> <role>`. Without
581
- TLS, the startup log warns that the session cookie crosses the network in clear
582
- text. Pair accounts with one of the TLS options below, or with a loopback bind and
583
- an SSH tunnel.
586
+ Then create personal accounts with `npm run user -- create <name> <role>`. HTTPS is on by
587
+ default with the monitor's own certificate; if you turn it off (`BLOCKYARD_TLS=0`) on a bind
588
+ that crosses the LAN, the startup log warns that the session cookie crosses the network in
589
+ clear text. Pair that with a TLS-terminating proxy, or with a loopback bind and an SSH tunnel.
584
590
 
585
591
  Accounts plus two node writes that operators may run:
586
592
 
@@ -596,7 +602,8 @@ Accounts plus two node writes that operators may run:
596
602
 
597
603
  ### TLS
598
604
 
599
- Built-in HTTPS on every listener:
605
+ HTTPS is on by default, with a certificate the server makes for itself. A certificate of your
606
+ own, on every listener:
600
607
 
601
608
  ```json
602
609
  {
@@ -632,9 +639,9 @@ matters here too: it makes the proxy the only way in.
632
639
 
633
640
  ### Binding to specific addresses
634
641
 
635
- The default `0.0.0.0` listens on every IPv4 interface the machine has, including VPN
636
- tunnels and container bridges. To serve exactly one LAN address and one VPN
637
- address, and admit only clients from those networks:
642
+ The default `127.0.0.1` answers this machine only. `0.0.0.0` listens on every IPv4
643
+ interface the machine has, including VPN tunnels and container bridges. To serve exactly
644
+ one LAN address and one VPN address, and admit only clients from those networks:
638
645
 
639
646
  ```json
640
647
  {
@@ -661,9 +668,11 @@ Things to know about specific binds:
661
668
  - For this machine only, use `"hosts": ["127.0.0.1"]` and reach it with
662
669
  `ssh -L 21000:127.0.0.1:21000 user@monitor-host`.
663
670
 
664
- ### Markets off
671
+ ### Markets off, for good
665
672
 
666
- No outbound connections except to the node:
673
+ Out of the box there are no outbound connections except to the node, because market polling is
674
+ a checkbox that ships unticked (**Display settings → Markets & Price → Enable market polling**). To be certain a machine never reaches out whatever anyone
675
+ ticks, remove the feed from the server:
667
676
 
668
677
  ```json
669
678
  {
@@ -713,7 +722,7 @@ sudo systemctl edit blockyard
713
722
 
714
723
  ```ini
715
724
  [Service]
716
- Environment=BLOCKYARD_AUTH=1
725
+ Environment=BLOCKYARD_BIND=192.0.2.10
717
726
  Environment=BLOCKYARD_DATA=/var/lib/blockyard
718
727
  ```
719
728
 
@@ -738,7 +747,7 @@ accounts and chart history, and never commit or publish it.
738
747
  | `audit.1.jsonl` … `audit.N.jsonl` | the server | sensitive | Rotated audit files, newest first. There are at most `store.auditKeep` of them, and rotation happens at `store.auditMaxBytes`. |
739
748
  | `history.json` | the server | no, but reveals node details | Snapshot of chart series, events and recent blocks, restored on startup and pruned to `store.retentionHours`. Can be tens of MB. |
740
749
  | `pool-aliases.json` | **you**, optionally | no | Hand-written display names for mining pools: a JSON object from pool key to label, for example `{"examplepool": "Example Pool"}`. The pool key is the lowercased coinbase tag the Mining page shows, or `unknown:<hex>` for blocks without a readable tag. Absent by default, in which case the coinbase text is shown as written. |
741
- | `pool-map.json` | `node scripts/pool-map.js` | no | Coinbase-tag-to-pool-name map built from the public mempool/mining-pools data set (MIT), with source URL and content hash. **A copy ships in `config/pool-map.json`** (151 pools), so pools are labelled from the first start; one here, written by the script (which needs network access), **overrides** the shipped copy. The server reads `BLOCKYARD_POOL_MAP` if set, else `<store.dir>/pool-map.json` if it exists, else the shipped file. The script always writes `<repo>/data/pool-map.json`, so move it, or set `BLOCKYARD_POOL_MAP`, if you use a different `store.dir`. `--file <pools-v2.json>` builds it offline. |
750
+ | `pool-map.json` | `node scripts/pool-map.js` | no | Coinbase-tag-to-pool-name map built from the public mempool/mining-pools data set (MIT), with source URL and content hash. **A copy ships in `config/pool-map.json`** (151 pools), so pools are labelled from the first start; one here, written by the script (which needs network access), **overrides** the shipped copy. The server reads `BLOCKYARD_POOL_MAP` if set, else `<store.dir>/pool-map.json` if it exists, else the shipped file. The script always writes `<repo>/data/pool-map.json`, so move it, or set `BLOCKYARD_POOL_MAP`, if you use a different `store.dir`. `--file <pools-v2.json>` builds it offline. Before replacing an existing file it prints the pools added, removed and changed and writes only with `--yes` or a "y" at the prompt; `--expect-sha256 <hex>` refuses a download that is not exactly those bytes; redirects are followed to HTTPS only. |
742
751
  | `fake-node.log` | development mode only | no | Log of the simulated node when `BLOCKYARD_FAKE_NODE=1`. |
743
752
  | `*.tmp` | the server | as the target file | Short-lived files from atomic writes (write, fsync, rename). A leftover one after a crash is safe to delete. |
744
753
 
package/docs/DEFECTS.md CHANGED
@@ -744,7 +744,10 @@ Kept as checked rather than deleted, so nobody re-derives them.
744
744
  budget has no room for a fourth figure and rules 4/9 forbid merging them. They now
745
745
  appear as three separately-labelled rows behind `detail`, where "not printed by this
746
746
  build" is a possible answer and the absence is visible.
747
- - [ ] **An interrupted index build starts over.** Noted 2026-09-14. `buildIndex` empties its output
747
+ - [x] **An interrupted index build starts over.** Noted 2026-09-14. Fixed 2026-09-16: the build
748
+ keeps `build-journal.json` beside its output and resumes per scanned block file and per sorted
749
+ bucket, cutting the buckets back to what the journal proves (`server/chain/index/build.js`, THE
750
+ BUILD JOURNAL). What was noted: `buildIndex` empties its output
748
751
  directory before it begins (`server/chain/index/build.js`), and the server treats an index as
749
752
  built only when `manifest.json` exists (`server/main.js`), so a build stopped by Ctrl-C, a crash
750
753
  or a reboot keeps nothing of its scan: the installer says so when it is stopped, and the server's
@@ -137,8 +137,8 @@ node scripts/index-build.js --out data/index --workers 4
137
137
  ```
138
138
 
139
139
  Last question: **start BlockYard now, in this terminal?** — yes runs it right there (Ctrl-C
140
- stops it, and stops a background build with it; there is no resume, so it starts over on the
141
- next start); `--start` does the same without asking.
140
+ stops it, and stops a background build with it; the next start resumes the build where it
141
+ stopped); `--start` does the same without asking.
142
142
 
143
143
  Scripted, with no questions (a fresh machine, a Makefile):
144
144
 
@@ -147,10 +147,17 @@ node scripts/setup.js --yes --rpc-url http://127.0.0.1:8332 \
147
147
  --datadir "$HOME/Library/Application Support/Bitcoin" --index-dir "$PWD/data/index" --workers 4
148
148
  ```
149
149
 
150
- Every question has a flag: `--datadir`, `--rpc-url`, `--label`, `--rpc-user` / `--rpc-password`,
151
- `--host`, `--port`, `--index-dir`, `--workers`. `--build-here` builds in the terminal and
152
- `--build-later` leaves it to you; `--start` boots the monitor at the end; `--force` replaces an
153
- existing `config/local.json` (with a backup).
150
+ Every question has a flag: `--datadir`, `--rpc-url`, `--label`, `--rpc-user` /
151
+ `--rpc-password-file`, `--host`, `--port`, `--index-dir`, `--workers`. `--build-here` builds in the
152
+ terminal and `--build-later` leaves it to you; `--start` boots the monitor at the end; `--force`
153
+ replaces an existing `config/local.json` (with a backup, also mode `0600`, and git-ignored).
154
+
155
+ The RPC password, when the node needs one: `--rpc-password-file PATH` reads the first line of a
156
+ file, and `--rpc-password -` reads it from stdin (with `--yes`, e.g.
157
+ `pass show node/rpc | node scripts/setup.js --yes --rpc-user monitor --rpc-password - ...`).
158
+ `--rpc-password P` still works but prints a warning: a password on the command line is visible to
159
+ every user of the machine in `ps` and stays in your shell history. Typed at the interactive prompt,
160
+ it is not echoed.
154
161
 
155
162
  ## 5. Run it
156
163
 
@@ -158,12 +165,15 @@ existing `config/local.json` (with a backup).
158
165
  npm start
159
166
  ```
160
167
 
161
- The log says `BlockYard 0.0.9 listening on http://127.0.0.1:21000`, then `address index: building
168
+ The log says `BlockYard 0.1.1 listening on https://127.0.0.1:21000` (its own self-signed
169
+ certificate, made on this first start; the browser warns once and remembers it), then `created the
170
+ first admin account (admin)` with a generated password **shown once** — copy it, or set
171
+ `BLOCKYARD_ADMIN_PASSWORD` before the first start to choose it — then `address index: building
162
172
  /Users/you/blockyard/data/index from main's block files with 4 workers -- the Overview shows the
163
173
  progress` (and `address index build: paused while the node's RPC is answering in … s` /
164
174
  `resumed` if the node struggles) and, when that is done, `address index built: … rows to block N
165
175
  in … min -- address pages are live` followed by `address index /Users/you/blockyard/data/index:
166
- following main from block N`. Open <http://127.0.0.1:21000>. The
176
+ following main from block N`. Open <https://127.0.0.1:21000> and sign in as `admin`. The
167
177
  Overview fills in within about thirty seconds; Block space lands a little after. Open Explorer,
168
178
  click the latest block, then any output address: while the index is building the page says so
169
179
  with the progress; once it is built, its balance, history and unspent outputs appear.