All notable changes to sentinel-node-tester. Format follows
Keep a Changelog; this project follows
Semantic Versioning.
- Refund-reconciled spend totals (per-GB / per-Hour P2P). After every
batch's
submitBatchCancel,audit/pipeline.jsnow reads the wallet balance and compares to the expected balance (balanceUdvpn − spentUdvpn). Any positive delta is treated as a refund credit:state.refundedUdvpnaccumulates the cumulative refund,state.spentUdvpnis reduced by the same amount so the live spend tile shows net spend, and a↩ Refund credited: X.XXXX P2P (cumulative refunded: …; net spend: …)log line is broadcast. BothrefundedUdvpnandestimatedTotalCostare now inPUBLIC_STATE_KEYS, andadmin.htmlrenders a new greenRefundedheader tile next toSpend(hidden as--until the first refund lands). Avoids relying on Sentinel's settlement-blockEventRefund(which is emitted by the chain itself, not by the user TX, and therefore not visible totx_search). - Per-GB / per-Hour suffix in the P2P mode badge. Both
admin.htmland/livenow render the active pricing mode at the end of the P2P badge detail line — e.g. "All online nodes — direct peer-to-peer payments per session : Testing All Nodes - Paying Per GB" (and Per Hour in the hourly variant). Readsstate.pricingMode(already in the public sanitize whitelist), defaults to per-GB if absent. Subscription-plan and Test-Run badges are unchanged. /liveETA readout next to the progress bar — same linear projection admin uses (remaining * elapsed / tested), formattedHH:MM:SS. ShowsETA —until snapshot size and at least one tested row are known. Ticks every second between SSE events for a smooth countdown.- Human-readable error labels in the public failure popup.
public.htmlnow maps DBerror_codestrings (HANDSHAKE_TIMEOUT,TCP_PORT_DEAD,SOCKS5_NO_CONNECTIVITY, etc.) to plain-English labels via a newerrLabel()/ERR_LABELShelper. The raw code stays alongside the label in monospace so the operator can still grep logs by canonical code. Unknown codes fall back to a Title-Cased version of the raw string instead of bareUNKNOWN. - On-chain reporting status dot.
admin.htmltoggle button now shows a red/green dot at the front: red whenonchainEnabled=false, green (with soft glow) when on. Button background stays transparent — only the dot lights up. Bug found at the same time: page-load state readd.onchainEnabledfrom/api/settings, but the response is{settings:{...}}, so the indicator was always rendering OFF; fixed to readd.settings.onchainEnabled.
- Boot-time silent zombies eliminated.
server.jscould deadlock during module init (top-levelawait import('./platforms/windows/wireguard.js')runs syncexecSyncprobes;emergencyCleanupSync()called at module scope blocks the event loop on slow Service Control Manager) while stdout was block-buffered when redirected — symptom: process alive at ~93MB RAM, idle CPU, no port bound, zero log output. Fixes:server.jsforcesprocess.stdout._handle.setBlocking(true)at the top of the file so console.log flushes immediately even when redirected.- The platform-wireguard
import()is now wrapped in a 5sPromise.racetimeout; on timeout the server falls back toWG_AVAILABLE=falseand logs[boot] WireGuard import failed. emergencyCleanupSync()no longer runs at module scope — it's deferred viasetImmediateinside theapp.listencallback so a slow SCM can't gate startup.process.on('uncaughtException'|'unhandledRejection')now print full stack traces (reason?.stack || reason?.message || String(reason)) andprocess.exit(1)after cleanup. Previously they ran cleanup and let the event loop continue on a half-initialised state — the silent zombie pattern.- State-snapshot restore
catch {}at boot now logs the error.
- DB lock leaks from ad-hoc scripts.
core/db.jsaddsPRAGMA busy_timeout = 5000so writers wait instead of failing immediately withSQLITE_BUSY, and registersprocess.on('exit', closeDb)so any process that importscore/db.js(includingnode -e "import('./core/db.js')..."verifiers) releases the WAL lock cleanly on exit. Previously a hung verifier could lockaudit.db-waland block server boot indefinitely. - Silent error handlers logged with context. Several previously empty
catch blocks now surface their cause:
core/chain.jsgetRpcClient/cleanupRpc/disconnectRpc,core/db.jsuseDbPRAGMA failures andwal_checkpointclose failures,audit/continuous.js_getDb().catch,audit/pipeline.js_flushOnchainBatch().catch. Cleanup-only catches in pipeline tunnel teardown intentionally remain silent. /livesticky column-header band was translucent during scroll. The th box-shadow only painted the 6px gap above each cell, so when body rows scrolled past, the 4pxborder-spacingstripe below the header row let rows of data flash through the band. Added a downward0 6px 0 0 --bg-card-solidshield (plus a 1px--borderunderline at +7px) so the header reads as a fully opaque band on every scroll position in both themes.- History pills (
Last 10 Baseline Readings/Last 10 Node Speeds) rendered as run-on digits with no separation/colors, e.g.4.620.920.5…. Two independent bugs:.h-pill-good/.h-pill-badreferencedvar(--accent-green)/var(--accent-red)tokens that don't exist insentinel.css— the actual tokens are--green/--green-bright/--green-dim(and--redequivalents). The pillcolorresolved to the inherited body color and the rgba backgrounds were too faint to register as pills. Switched to the real tokens (and dropped hardcodedrgba()fills in favor of--*-dim). Same fix applied inindex.html.runSubPlanTestandrunPlanTestinaudit/pipeline.jsdid not resetstate.baselineHistory/state.nodeSpeedHistory(and several counters) on a fresh run, so subscription-plan reruns kept appending to the prior run's pills instead of starting clean. Both runners now zeropassed10/passed15/passedBaselineand clear the two history arrays in the sameif (!resume)block wheretestedNodes/failedNodesreset.renderHistory()is now also defensive against legacy number-only entries (coerces toNumber(e)whene.mbpsis missing) so a stale snapshot from before the{mbps,ts}schema can't crash the render.
/livebaseline column was always--. Root cause:audit/continuous.js_sanitizeBatchNodeResultstrippedbaselineAtTestfrom the public payload, sobatch:node:resultarrived without baseline. Now forwards it asbaselineMbps. Server's public sanitizer (server.js:1180) was already passing it through, andlive.htmlalready readsd.baselineMbpson the SSE upsert — column populates immediately./livebaseline column reverted to--on page refresh and on every historical row. The previous fix only patched the live SSE forward path. The DB schema itself had nobaseline_mbpscolumn, so refresh / historical hydration (/api/public/runs/currentand/api/public/runs/last) returnednullfor every row. Added migration v9 tocore/db.js:ALTER TABLE results ADD COLUMN baseline_mbps REALand the same onbatch_results.mapResultToRow+_insertResultSqlnow persistr.baselineAtTest;insertBatchResultacceptsbaselineMbps | baseline_mbps | baselineAtTest; the second batch_results writer inserver.js(the inlinewithBatchTrackingmiddleware) writesbaseline_mbps: r.baselineAtTest ?? r.baselineMbps ?? null; andgetActiveBatch/getBatchWithNodes/getLastBatch/getBatchResultsall projectbaseline_mbps AS baselineMbps. Migration is idempotent (PRAGMA-checks the column before ALTER) so re-runs on a v9 DB are no-ops./liveprogress bar jumped to 100% on the first row.cbRender()usedtotal = snap || testedas the denominator, so before the snapshot-size arrived frombatch:start, every new row madetested === totaland the bar pinned to 100%. Fixed by gating pct onsnap > 0; bar holds at 0% until the snapshot is known, then trackstested / snapexactly. Counter card and bar now read from the same single source./livepager stuck at "Page 1 of 3 — 1–50 of 142" on a fresh test. Rehydrated rows fromlocalStoragesurvived into a new run because thebatch:startreset only fired when a prior_cb.batchIdwas already set. Reset now also fires when_cb.batchId == nullANDresultsArris non-empty — i.e., we just refreshed mid-old-run and a new batch is starting.- Admin Mbps column drift on rows with the ⚡ ISP-bottleneck glyph. The
glyph was appended after the Mbps string with no width reservation, so
rows with
ispBottleneck=truerendered wider than rows without and broke right-alignment. Fixed by wrapping the value in a flex inline container with a fixed-width 14px slot for the glyph (empty string when absent), so all rows allocate the same horizontal space. - Admin Live Log was capped at 400px.
.logshad a hardmax-height: 400pxand the parent.log-containerdidn't flex to fill the page. Nowmin-height: 320px,max-height: calc(100vh - 240px), withflex: 1 1 autoon both the container and the log body so it stretches to the viewport instead of stopping at a fixed pixel cap.
- Duplicate on-chain reporting controls inside the P2P Payment Settings
drawer. The per-GB / per-Hour
⚙ SETTINGSbutton opened a drawer that duplicated the Enable / Nodes-per-report / Region / Recent-reports block already owned by the standalone "On-Chain Reporting" header popup (openOnchainPopup). Two surfaces for the same settings was a footgun (last-write-wins drift between drawers). Removed the on-chain section, its reset/save hydration, and the Recent-reports refresh handler fromopenP2pSettingsDrawer. HeaderOn-Chain Reportingbutton is now the sole entry point. - Inline
error_codechip from the live admin log and the per-row failure-error column.live.htmlno longer renders the small[ERR_CODE]chip in inline log entries, and the admin/live node-detail drawers no longer include theError Coderow. Per-row clipboard copy blocks still include the canonical code (failure-log MUST is preserved); only the visible UI chip/row was dropped pending the redesigned logs panel.
- On-chain memo format switched from binary base64 to legible CSV (v2).
Old format was opaque on p2pscan (a base64 blob). New format is plain ASCII
the operator and any consumer can read without a binary decoder:
Header carries the tester's baseline once (not per record). Per-record fields: full bech32 address, ok=1/0, measured Mbps (one decimal, empty for failed), concurrent peers at test time, handshake latency in ms. Pipeline now uses a greedy packer (
SNTR1|v2|<region>|b=<baselineMbps>|t=<unixSeconds> <addr>|<ok>|<mbps>|<peers>|<lat> …packBatchincore/onchain-report.js) that fits as many records as the 256-char memo allows — typically 4–5 per TX depending on value lengths, vs. the old hard-coded 6. Records that don't fit roll into the next TX. The decoder still understands v1 binary base64 memos so historical TXs render correctly in the history popup.
- On-chain reporting was completely broken in 1.4.0. Every batch broadcast
failed silently with chain code 12 (
memo too large: maximum number of characters is 256 but received 956 characters). Root cause:MAX_RECORDSwas set to 50 but Sentinel's chain enforces a 256-character TX memo limit. Base64 encodes 3 raw bytes into 4 chars, so a 256-char memo holds at mostfloor(256/4)*3 = 192raw bytes. With our 15-byte header + 28-byte records, the true ceiling isfloor((192-15)/28) = 6records per batch. ReducedMAX_RECORDSto 6 incore/onchain-report.jsand added a defensiveMEMO_CHAR_LIMIT = 256guard incommitBatchthat throws before broadcast if the encoded memo would exceed the chain cap. On-chain reports now post successfully and are visible athttps://p2pscan.com/transaction/<hash>. core/settings.jsonchainBatchSizedefault lowered from 25 to 6 andsanitize()now clamps the value to1..6(was1..50). Existing settings with higher values are auto-clamped on read; aPOST /api/settingswith a legacy value of 25 persists as 6 silently.audit/pipeline.js_flushOnchainBatchslice cap reduced from 50 to 6 so the in-memory buffer can never present an oversized batch to the encoder.admin.htmlfailure-log copy buttons acrossadmin.html,public.html, andlive.htmlnow checkres.okbefore parsing JSON, so a 404/500 from/api/public/node/:addr/errorsproduces a clean error toast instead of a silent JSON parse exception.
- p2pscan.com TX links wired through three surfaces so the operator can
click a hash and view the on-chain report:
audit/pipeline.js_flushOnchainBatchnow appends a clickable URL to the broadcast log line:📡 On-chain report posted: N nodes, MB @hH → https://p2pscan.com/transaction/<hash>admin.htmlRecent Reports list (bothdata.reports.mapblocks) renders the TX hash as an<a target="_blank">styled with--accent.admin.htmlandlive.htmlappendLognow auto-linkify everyhttps?://...URL inside any broadcast log message — escapes HTML first, then wraps URL spans with<a target="_blank" rel="noopener">.
(?)info popup in admin.html on-chain reporting section corrected: now states "up to 6 records (244 base64 chars) and costs ~200,000 udvpn gas" instead of the prior "up to 50 records" claim, and the wire-format spec linecount 1B uint8 (≤50)is nowcount 1B uint8 (1–6, chain memo cap)to match the encoder.CLAUDE.mddocuments the chain memo cap explicitly: batch size 1–6 (default 6) "capped because Sentinel's chain enforces a 256-char TX memo limit and 7 binary records would overflow base64", with a parallel note oncore/onchain-report.js's key-files entry calling out chain rejection code 12 by name.
core/onchain-report.jsheader comment rewritten to walk through the 256-char chain memo limit, the base64 ratio, and the math that produces the 6-record ceiling — so future Claude sessions don't reintroduce the bug by raisingMAX_RECORDS.admin.htmlsettings hint under#setOnchainBatchSize(and the matching#ocBatchSizeinput) explains the 256-char chain memo cap to operators. Inputsmin="1" max="6"enforce it at the form level.
- Cross-platform
SETUP.mdcovering Windows, macOS, and Linux. .gitattributesenforcing LF line endings on source files and CRLF on Windows-only scripts.start.shlauncher for Linux/macOS (re-execs under sudo for WireGuard).CONTRIBUTING.mdand thisCHANGELOG.md.- Real
platforms/linux/README.mdandplatforms/macos/README.md(replacing placeholders). .env.examplenow documents every variable read by the code, includingLISTEN_HOST,LCD_ENDPOINTS,DNS_SERVERS,INSECURE_COOKIE,ENABLE_HSTS,ALLOW_PUBLIC_TEST, and the public-test plan/sub fields.package.jsonfileswhitelist now shipssentinel.css,about.html,index.html, thefonts/directory, andscripts/cleanup-runaway-runs.mjs.core/onchain-report.js— on-chain performance oracle. Every N tested nodes, the tester self-sends 1 udvpn with a compact binary memo (SNTR1magic + version + region + baselineMbps + startedAt + count + records). Includes encoder, decoder, RPCcommitBatchbroadcaster, and RPCtx_searchqueryReportsconsumer. Opt-in viaonchainEnabledsetting.core/settings.js— runtime-mutable settings (gigabytes,batchSize,autoCancelAfterTest,maxPriceUdvpn,onchainEnabled,onchainBatchSize,onchainRegion) withclampIntsanitization on read and write.bin/commands/universal-test.js— single CLI entrypoint that probes node reachability across SDK paths.core/db.jserror_logs.raw_jsoncolumn (migration v3) — captures the full diagnostic blob alongside the truncated message so the failure-log popup can render structured diag fields (status, transports, last attempt stage) without losing the raw payload.- Failure-popup enrichment across
admin.html,public.html, andlive.html: the per-row copy button now produces a multi-line block with sections for Node / Address / Stage / Error code / Captured / Message / Log snippet / Diag (parsed fromraw_json). live.htmllog-filter NODE/FAIL/SYS regex now matches the actual broadcast prefixes (was over-restrictive, hid lines that started with emoji glyphs or[scope]tags).- TEST RUN flag (
testRun: truebody or?testRun=1query) onPOST /api/start. Writesmode='test'rows to the singleaudit.db.
- Single-mode collapse: removed dual-mode (dev/bundled/public) system in
favor of a single mode plus a
broadcastLivetoggle. One database (audit.db), one set of routes. /api/admin/public-test/*endpoints removed.state.broadcastLiveboolean controls whether public SSE //livereflect the in-flight audit or the last-completed snapshot. Toggled viaPOST /api/broadcast.
- 70+ ad-hoc dev scripts under
scripts/(analyzers, probes, retests, plan checkers, dump utilities). The published package now ships onlypostinstall.js,backfill-runs.mjs, andcleanup-runaway-runs.mjs. - Stale
csharp-bridge/project and its launcher (SentinelAuditLauncher.cs) — the C# SDK path was decommissioned. - Legacy
tools/smoke-public-mode.mjs(referenced removedPUBLIC_MODEhelpers from before the mode collapse). - Local-only artifacts:
fresh-clone-test/,agent-map.json,suggestions/, duplicate V2Ray binaries inbin/, root-level*.logfiles.
See git history.