Skip to content

chore: sync develop into develop-v2 (supersedes #1031) - #1039

Merged
VijitSingh97 merged 40 commits into
develop-v2from
sync/develop-into-v2-0816b
Aug 16, 2026
Merged

chore: sync develop into develop-v2 (supersedes #1031)#1039
VijitSingh97 merged 40 commits into
develop-v2from
sync/develop-into-v2-0816b

Conversation

@VijitSingh97

Copy link
Copy Markdown
Collaborator

Supersedes #1031, which went stale when #1035 landed on develop-v2. Fresh merge from the current tips.

Eleven conflicts, every one decided against the code rather than by preferring a branch, with the evidence cited in the commit message. Highlights:

Gates

Gate Result
conflict markers none
make lint exit 0
tests/stack 2333 passed, 0 failed
frontend 375 passed, 0 failed
dashboard 1818 passed, 96.79% coverage

One judgment call surfaced rather than smoothed over: the is_appliance-before-cosign ordering in control_upgrade is reasoned, not pinned by an existing fixture.

VijitSingh97 and others added 30 commits August 14, 2026 17:46
The fixture drift guard compared only top-level build_state() keys, so
nested payload growth (a new field under an existing section) left
tests/frontend/fixtures/state.json stale and every component gated on
the new field rendering its empty state across the frontend suite and
the visual harness.

The guard now reruns _gen_state.py (deterministic) as a subprocess to a
temp file and diffs sorted dotted key paths — the Python mirror of
CONFIG_KEY_PATHS_JQ in tests/integration/lib.sh — in both directions.
Shape only, never values, so payload value tweaks do not churn the
test. Subprocess, not import: the generator patches time.time
process-wide. _gen_state.py takes an optional output-path argv for
this.

Closes #974.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After the restore brings the baseline back up, the run no longer trusts
"healthy": a stack recreated with harness-rendered creds is internally
consistent, mines fine, and 401s every host-side RPC probe — the 2026-08
incident. Two checks now bind in the EXIT-trap epilogue:

- env bake: the MONERO_NODE_PASSWORD line baked into the running
  dashboard container (docker inspect) must equal the on-disk .env line
  in RESTORE_DIR. The compare is env_bake_verdict in lib.sh — whole
  KEY=VALUE lines in, a verdict word out, values never printed — pinned
  by selftest.sh fixtures (match/mismatch/no-disk-value/not-baked).
- host RPC: monerod must answer get_info with the on-disk creds (the
  exact probe the incident broke). The probe script travels over ssh
  stdin (bash -s) so creds stay on the box and the remote command
  string stays paren-free; only .status==OK is required, polled 60s.

A failed proof — including a failed baseline apply/up, which previously
could exit 0 on a passing run — warns RESTORE NOT PROVEN, names the
docker-compose-up-from-disk recovery, and exits 1 from the trap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
build_xvb_calc no longer collapses to {"enabled": false} when XvB is off:
the tier table, draw odds, and study-prior band are computable from local
config plus the cached public feeds, and the table is the enable/don't-
enable decision aid — hiding it behind the flag defeated its purpose
(#938). The flag still stops every xmrvsbeast.com fetch (the #726 egress
rule), so a never-enabled box honestly degrades to tier costs only and a
just-disabled box ages out through the existing staleness rules; the
live-credit context goes quiet on its own because build_state computes
realization only while enabled and Metrics reports "Disabled" tiers.

Client: computeXvbTier and xvbDecisionRows drop their enabled gates (the
what-if runs from local hashrate), XvbTierBlock renders when disabled
minus the Current/Target live-credit cards plus an off-explainer line,
and the earnings card keeps the XvB tab whenever the payload carries
tiers — enabled:false still keys every live-donation surface (stats
card, header split, Overview tiles, hero KPIs) off, unchanged.

Closes #938

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
testing-strategy.md's "What runs where" table promised a per-PR
test-inventory drift check, but #414 deleted the drift machinery along
with the committed file — tests/inventory.sh only generated, and always
exited 0. The drift that can still exist is the generator drifting from
the codebase: the gathering is grep-based, so a suite that moves or
changes shape enumerates as zero without erroring.

Make that failure real: after gathering, exit 1 if any suite count is
zero (with :-0 defaulting, since grep -c on a deleted file emits nothing
rather than 0). Run the script in the existing `shell` CI job, output
discarded — the generated file stays git-ignored per #414. Reword the
doc's table row and "test code is real code" bullet to claim exactly
this check and no more.

Not touched here: the diff-cover vacuous pass on shell-only diffs and
the push-trigger branch list, both named in #981 as adjacent gaps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ct, mixed time registers

Expected-vs-actual pct: build_earnings_vs_actual withholds pct past 999%
instead of publishing the raw ratio — a box idle for most of the window
divides normal confirmed payouts by a near-zero expectation and renders
five digits that read as a bug. pct=None with available+enabled both true
happens only on this path, so ExpectedVsActualCard appends the
explanation to the row tooltip without a new payload flag.

Tari per-block reward: computeEarnings gated tariRewardPerBlock on
tari_available (and on a positive what-if hashrate via the early
return), while TariCard prints metrics.tari_reward unconditionally on
the same page. The reward is a chain fact, so it now passes through
whenever p2pool reported it; time-to-block and the per-day averages
keep their channel/hashrate gates. The fiat '≈ per Block' follows since
it derives from the same field.

Last-block register: Global P2Pool Stats showed format_time_abs — a
bare HH:MM:SS with no date or timezone cue — two cards from the cadence
card's relative duration. last_blk (its only consumer) now renders
'<duration> ago' from the same format_duration, 'Never' before the
pool's first block; the cadence tooltip keeps the absolute anchor.

Closes #992.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…#904)

xmrig-proxy, dashboard, caddy, docker-proxy and docker-control reported Up
with no health signal, so a dead-inside service (the v1.8.1 incident: the
dashboard serving 502s behind an Up container) was invisible to
`pithead status` and the container-health alert (#337). Each probe uses only
tooling its image already ships and reflects readiness, not process-up:

- xmrig-proxy: in-image script TCP-connects to the HTTP API port (bash
  /dev/tcp, port from env per the #90 no-creds-in-compose rule). The API is
  the honest probe: a failed stratum bind kills the process outright, while
  the API — what the dashboard polls — can die quietly.
- dashboard: in-image script HEADs /api/state on the fixed 127.0.0.1:8000
  bind with the venv's python3/urllib (slim image has no curl/wget); a 200
  proves the server answers where Caddy proxies AND state assembly works.
- caddy: busybox wget against the admin API's default localhost:2019 —
  200 from /config/ means running with a loaded config. Not a vhost probe:
  scheme/port/auth vary with config and busybox wget can't speak the
  internal-CA TLS.
- both socket proxies: busybox wget GET /_ping THROUGH HAProxy to the
  Docker daemon — proves the proxy brokers the socket. /_ping is in the
  image's default-allow set (PING=1), independent of the POST gating, so
  the rulesets stay untouched.

No flapping or gating changes: none of the five has dependents, `pithead up`
and the one-click upgrade never wait on health (`compose up -d`, no
--wait), `pithead status` counts "starting" as pending rather than a
problem, and the #337 alert already debounces 120s of continuous unhealthy.
The dashboard's start_period covers app start plus a SQLite auto-heal
rebuild so a warmup blip never false-fails a one-click upgrade's status
view (the #622 lesson). All five probes verified against the real pinned
images (healthy under cap_drop ALL + read_only + no-new-privileges; closed
port / stopped app fails).

Tier-1 coverage: tests/stack/test_compose.sh now asserts every rendered
service carries a healthcheck, pins the five new probe commands, and checks
no probe interpolates the proxy auth token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#972)

A tor container restart/recreate kills every SOCKS connection, and monerod
keeps the dead peer sockets: bench-observed at 0 in / 0 out peers for ~6h,
get_info synchronized:false, height creeping, every healthcheck green. A
plain monerod restart re-dials through the fresh tor and recovers in under
a minute — so couple the restart to everything that cycles tor, and alert
on the strand for the paths that can't be coupled.

Recovery (root cause — tor cycling strands monerod):
- compose: monerod's tor dependency gains restart: true, so every compose
  operation that restarts/recreates tor (up/apply/upgrade recreation,
  'pithead restart tor') restarts monerod right after tor is healthy again
  (verified against compose v2.40: both the restart and the recreate path
  propagate, health-gated). Guarded by new test_compose.sh invariants.
- tor_heal (#424 auto-heal) bypasses compose, so a successful tor heal now
  cycles monerod itself when the node is local (stop timeout 60s matches
  monerod's stop_grace_period; HTTP timeout outlasts it per #234). A failed
  monerod cycle warns and never refunds the tor budget slot.
- new 'pithead restart monerod' leg: the manual re-dial for tor restarts
  that happened outside the stack (docker daemon restart, manual restart).

Detection (for whatever still slips through):
- MoneroClient.get_sync_status passes the raw get_info `synchronized` flag
  through — required because a stranded node can report a stale
  target_height of 0, which the height math reads as "synced". Only the
  RPC path carries the key; log-scrape/remote paths stay verdict-free.
- data_service debounces it with the existing NodeHealthMonitor
  (down_after=NODE_STALE_AFTER_SEC, 600s env-tunable): ever_up doubles as
  "ever synchronized", so initial sync never alarms. Surfaced as
  monero_sync.stale in the snapshot and fed to the alerter.
- alert_service emits the out-of-sync/back-in-sync edges through the
  existing node_down/node_recovered toggles (no new config surface, no
  reference.json change) and tallies the incident for the daily digest.
- doctor: new Monero sync check probes get_info from the host with the
  .env digest creds (monerod image has no curl) and WARNs on
  synchronized:false, naming 'restart monerod' as the fix. WARN not FAIL:
  initial sync reads identically.

Tier 1 covers all decision logic (pytest client/data/alert/tor_heal,
tests/stack doctor + restart + compose invariants), tier 2 the flag over
the wire; the real strand + re-peer remains bench-only (tier 4).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…blast radius, unify the node_down story

The docker-control comments documented the start/stop surface as
p2pool/xmrig-proxy + tor while tor_heal now also cycles monerod after a heal;
both comment sites name it. telegram.md's overview table still described
node_down as unreachable-only while its event-key table covered the
out-of-sync strand — the two now tell the same story.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r-loss

fix: restart monerod whenever tor restarts, and notice when it strands (#972)
…althchecks

feat(compose): healthchecks for the five services that had none (#904)
…ble-when-disabled

fix(dashboard): keep the XvB decision table visible while XvB is off (#938)
…e-proof

test(e2e): prove the restored stack runs the on-disk config (#971)
…ift-nested

test(dashboard): fixture drift guard pins every depth, not just top-level keys (#974)
…drift-ci

test(ci): the inventory drift check now exists — and inventory.sh can actually fail (#981)
…ities

fix(dashboard): three display honesty fixes — pct cap, known reward shown, one time register (#992)
dashboard.js ran its side effects at import, so it was the only frontend
module node --test could not even load. Wrap the whole client in an
exported initDashboard() whose parameters name the browser seams (DOM,
storage, fetch, history, timer, render) with the real facilities as
defaults, and guard the boot call on `document` — the browser runs
exactly the same statements in the same order; under Node the import is
side-effect-free. windowFromUrl/loadSeries take their inputs as
arguments and are exported alongside REFRESH_MS/FETCH_TIMEOUT_MS.

dashboard.test.mjs (22 tests) drives the real loop through fakes and
observes it via the props handed to renderApp: the pre-tick abort
invariant and the hung-poll/inflight recovery (#382), disconnected
state keeping the last snapshot, boot sequencing and preference
seeding, zoom/range/avg query building, and every App handler's state
transition + persistence + fetch/no-fetch contract. Render output stays
components.test.mjs's job; normalizer semantics stay logic.test.mjs's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… an overlap check (#1000)

diff-cover exits 0 with 'No lines with coverage information' whether the
diff is shell/docs-only (fine) or changed dashboard Python a stale
coverage.xml never measured (a green gate that proved nothing).

make test-patch-coverage now runs scripts/patch-coverage.sh: diff-cover
as before, then a file-level overlap check on its green paths. A diff
with nothing under build/dashboard/mining_dashboard/ passes loudly and
says the gate is not applicable; a changed measured file absent from
coverage.xml fails, naming the file. Comment-only changes to measured
files still pass — the file is present in the XML, so diff-cover's
no-measurable-lines verdict stands. Overlap logic is fixture-tested via
--self-test, driven from the stack suite like the operator-strings
guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…js-tests

test(frontend): the client entry point gets its tier — 22 tests via the issue's own seam refactor (#903)
…-vacuous

test(ci): the patch-coverage gate can no longer pass vacuously (#1000)
…ary (#1001)

RigForge writes noop (already on target) and throttled as first-class
terminal statuses since rigforge#320 (v1.12.0), and control-apply can end
terminal failed — but the upgrade poll matched only applied|rolled_back|
failed and the apply poll only applied|rejected|rolled_back. A noop or
throttled upgrade burned the full 90s poll cap and recorded "accepted —
upgrade still running"; a failed apply burned the 20s deadline and
recorded "accepted".

- Upgrade poll: noop and throttled are terminals now, recorded with the
  rig's own reason (the frontend already renders both statuses).
- Apply poll: failed is a terminal now (the rig could not restore its
  rollback backup), recorded with its reason and audited as failed.
- The legacy failed+"throttled…" remap stays for pre-v1.12.0 rigs
  (v1.11.2 is the supported floor) but is anchored to the leading word:
  a modern rig's genuine failed can mention the throttle too ("throttle
  state unavailable", rigforge#321's fail-closed refusal) and must stay
  a fault. Drop the remap when the fleet floor reaches v1.12.0 — the v2
  appliance bakes v1.15.0.

Stack suite gains 7 assertions across both polls, fail-first-proven:
exactly those 7 fail against the old poll cases (1704+7=1711 green).
Dashboard doc outcome lists updated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ll-vocab

fix(control): the worker pollers speak RigForge's full terminal vocabulary (#1001)
The Tari chain on the live reference deployment measured 149 GiB on
2026-08-15 (du -sh, gouda). The repo's own figure history makes the
growth rate linear: ~135 GB (2026-06-12, #232) -> ~139-140 GB
(2026-07-02, #334) -> 149 GiB, i.e. ~6.6-6.8 GiB/month over two
independent intervals. The 170 GiB budget's remaining headroom is
~21 GiB — about three months.

200 GiB restores ~51 GiB of headroom (~8 months at the observed rate).
The summed minimums move with it: ~330 GB pruned / ~530 GB full across
setup preflight, doctor, and every doc that cites them. Touched doc
lines adopt the develop-v2 reconciled measured figures (#1005: Monero
~270 GB full / Tari ~150 GB) so both branches read identically and the
develop -> develop-v2 sync merges clean. Mirrors PR #1011 (develop-v2).

Upstream's integration guide still advises "50GB+ SSD" for a base
node — a third of the live archival chain — so live measurement stays
the budget's source of truth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…abulary (#1009)

parse_worker_control_status filtered rigforge.control's mirrored outcome to
{applied, rejected, rolled_back} — a subset that predates rigforge#320's
noop/throttled terminals and #1001's own failed addition to the apply poll.
RigForge v1.15.0 now ships the #346 control mirror in its enriched feed for
the first time, so this stale filter is live: a mirrored failed outcome
drops silently, leaving a #185 change-history row "accepted" forever in
exactly the slow-terminal case the mirror exists to close.

- xmrig_client._CONTROL_TERMINAL: applied/rejected/rolled_back/failed/
  noop/throttled — the same six statuses pithead's own control_worker_apply/
  control_worker_upgrade poll cases accept (#1001). started (rigforge#320's
  in-flight upgrade marker) stays non-terminal alongside this dashboard's
  own accepted/running placeholders.
- storage_service.reconcile_worker_config_status: the same six-status
  allowlist (module constant _RECONCILE_TERMINAL, mirroring the client's
  _CONTROL_TERMINAL) gates the accepted-row UPDATE. status is recorded
  verbatim — the frontend's STATUS_META (workerview.mjs) already renders
  every member of this vocabulary, added for the #597 upgrade badge.
- docs/workers.md: the reconcile vocabulary list now enumerates all six
  outcomes, and the stale "RigForge does not ship this mirror yet" note
  is replaced with the v1.15.0 floor.

Dashboard suite gains 3 assertions, fail-first-proven against the old
allowlist (1703+3=1706 green). Patch coverage 100% on the 3 changed lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two tuples are declared separately on purpose (parser with the client,
allowlist with the store, neither importing the other for six strings) — so
nothing stopped them diverging. A status the parser accepts but the store
does not act on silently stops reconciling and freezes history rows at
accepted: precisely the bug the pair was just widened to fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dget-develop

fix: raise the Tari disk budget to 200 GiB
…ncile-vocab

fix(dashboard): the enriched-feed reconciler speaks RigForge's full terminal vocabulary (#1009)
…#942, #1002)

#942 adds five rows to the live e2e matrix, each following the existing
axis/scenario idioms in scenarios.sh + assert_running_state:

- tari.mode=remote (remote-tari-main-secure): the bundled tari
  container/onion absent, the sync gate proven against a remote target —
  needs --remote-tari-host, mirrors monero's own remote-mode handling.
- p2pool.stratum_tls=true (local-pruned-main-stratum-tls): a live TLS
  handshake on the published stratum port, and the served cert's
  fingerprint matches the one operators are told to pin.
- network.tor_egress_firewall=false (local-pruned-main-firewall-off): no
  pithead-tagged rule installed AND a real clearnet dial from a
  mining_net container succeeds — the mirror of the fail-closed default
  assert_egress_posture/fault_firewall_rollback already prove elsewhere.
- payout confirmation (local-pruned-main-payout-confirm): a
  "payout_confirm=env" marker resolves to a real monero.view_key (+
  optional tari pair) from IT_MONERO_VIEW_KEY/IT_TARI_VIEW_KEY/
  IT_TARI_SPEND_PUBLIC_KEY; asserts PAYOUT_CONFIRM_ENABLED and the
  dashboard's own earnings.confirmed.enabled read true (a real confirmed
  payout needs days no e2e run has).
- dashboard.secure=false + p2pool.pool=main (local-pruned-main-insecure):
  decouples insecure mode from its previously nano-only pairing.

expected_services/absent_services now gate tari on tari.mode (was
hardcoded always-on) and wallet-rpc/tari-wallet on view_key presence.

#1002 adds two legs to run_rigforge_control:
(a) --rigforge-upgrade: POSTs the rig's own already-installed version
    through /api/control/worker-upgrade and asserts noop — exercises the
    dashboard->host-runner->rig /upgrade chain (or its client-side
    shortcut, depending on the dashboard's poll-cache freshness) without
    ever rebuilding the rig, including the noop/throttled/failed
    vocabulary #1001 taught control_worker_upgrade's poll loop. The host
    independently re-derives "latest" and refuses any mismatch before
    dialing, so this can only ever land on noop or a safe rejected.
(b) a second writable-key edit, pools, beside the existing max_temp_c
    one. The enriched feed exposes no live readback for writable config
    values, so the restore target is the dashboard's own
    last_applied.pools record — the same source Worker Inspect's real
    editor prefills from — and the probe value is operator-supplied
    (IT_RIG_POOLS_PROBE) since pithead treats pools as opaque passthrough.

selftest.sh: 165 -> 201 assertions, all passing (36 new, pinning the
pure resolve_overrides/expected_services logic + scenario existence).
The live-only proofs (TLS handshake, firewall dial, payout wiring,
pools/upgrade legs) have no unit tier, same as every other
--rigforge-control leg in this file — they need bench validation.

docs/dev/integration-testing.md and docs/dev/testing-strategy.md
updated for the new axes/legs/flags; the "insecure + main matrix row"
known gap is marked resolved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ing upgrade history (#1013-#1015)

Three issues sharing one data plumbing seam (per-rig hashrate history + per-rig change
records), implemented together on one branch.

fix (#1014): a one-click rig upgrade left no per-rig record. handle_worker_upgrade returns
202 immediately (a rebuild can take minutes) and never awaited a result the way
handle_worker_apply does, so nothing ever called _record_worker_result for it. Now a tracked
background task (server.py's _finalize_worker_upgrade, referenced in app["_bg_tasks"] so it
can't be GC'd mid-flight, cancelled on app shutdown) waits for the host runner's terminal
result and records it — one row per upgrade attempt, worker + target version + terminal
status + reason — independent of whether the operator's tab stays open to see it land.
worker_config gains a `type` column ('apply' | 'upgrade', migrated forward for existing
installs) so get_last_applied_worker_config can exclude an upgrade's {"version": ...} marker
from the config-editor prefill, while get_worker_hashrate_by_config's existing
status=='applied' boundary logic picks up an applied upgrade as a version boundary for free —
hashrate no longer misattributes a build change to whatever config happened to be active.
The recordable-status set widens to include the upgrade-only noop/throttled terminals. The
change-history table's "Changed" column now reads "upgrade → vX.Y.Z" for an upgrade row
instead of the literal key name "version".

feat (#1013): Worker Inspect gets the fleet chart's range-selectable hashrate view, sized to
what a single rig's data can honestly support. /api/worker takes range=/from=/to= (mirroring
/api/state) and feeds get_worker_history through the existing _downsample_gauge_rows via
_gauge_series — no new table, no new collector. The client reuses chart.mjs's palette/gradient
idioms in a new WorkerChartCard, not a second charting approach: 24 Hr/1 Wk/All only (worker_history
samples ~5 min, so "1 Hr" would be dishonest; retention is 30 days, so "All" already means what
"1 Mo" would), no avg-window toggle (only h15 is stored). A rig with no samples yet renders an
explicit empty-state message instead of an empty axis. Read access rides the existing
DASHBOARD_CONTROL_ENABLED gate unchanged — deliberately deferred, see below.

feat (#1015): config applies and rig upgrades mark the chart, a fourth instance of the
hidden-0-1-axis scatter pattern Events/Raffle/Payouts already use. One marker dataset,
per-point pointStyle (diamond for a config apply, triangle for an upgrade) and colour (accent
for an applied outcome, muted for rejected/rolled_back/failed/throttled/noop/accepted — shown,
not dropped, so "we tried and it bounced" still explains a flat stretch). Markers are the same
range/window slice of worker_config as the hashrate line, so nothing renders outside the
chart's own visible window.

Deliberately deferred, with reasons:
- Read-gating: #1013 raises whether viewing a rig's own telemetry should require the control
  channel. Left riding the existing gate — widening a read boundary is a separate access-model
  decision, not a rider on a charting change.
- Accepted/rejected share-delta series on the worker chart: #1013 marks this optional ("out of
  scope unless it's cheap"); no per-poll differencing path exists for worker-level counters
  today, so it isn't.
- control_audit's `keys` sanitizer (naming the rig in a worker-upgrade audit row): #1014 itself
  flags this as "changed carefully or not at all" — a JSONL-injection chokepoint in the host
  bash script, out of scope for this Python/JS change.
- RigForge's own /status mirror for upgrades (the "second bug" #1014 flags as unverified): the
  worker_config row this change now writes would close that gap by construction if RigForge
  mirrors upgrades the same way it mirrors applies, but RigForge's source isn't in this repo to
  confirm the premise.

make lint: clean (sh/py/js/yaml/md/docs-voice/operator-strings/proto/toml).
make test-dashboard: 1717 passed, 97.01% coverage (gate 80%).
make test-frontend: 317 passed, 0 failed.
make test-patch-coverage: 100% on all 4 changed Python files (gate 90%).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ker-inspect-chart

feat(dashboard): per-worker hashrate chart with change markers, and the upgrade history row that was never written (#1013, #1014, #1015)
…ix-rows

test(integration): five live-matrix rows + the rig upgrade and pools legs (#942, #1002)
VijitSingh97 and others added 10 commits August 15, 2026 21:17
… key (#1023)

The runner's pre-download guard tested the LOCAL cosign.pub, so it was blind to
the one upgrade that needs it: an install cut before signing engaged moving to a
signed release. Prod hit this on v1.18.1 — the guard passed, the bundle
downloaded and extracted, and the abort landed inside the new CLI's image gate
after a full config re-render, leaving the operator to finish from a shell.

Every release bundle ships cosign.pub (make_bundle copies the committed key
unconditionally), and control_upgrade already refuses source checkouts up front,
so cosign is a flat precondition of a one-click upgrade. Check it beside the
source-checkout refusal: before the throttle claim and before the GitHub dial,
so a refusal costs nothing and the operator can install cosign and retry at once.

Tier 2 covers both halves of the widened condition — key-less and key-holding —
each asserting the refusal lands with nothing dialled, nothing extracted and no
throttle claimed. The $UPG fixture gains a cosign stub so the paths that are
meant to proceed no longer depend on whether the host happens to have one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…grade-precheck

fix(upgrade): require cosign before the one-click download, key or no key (#1023)
The docs were written when only monero.mode had a remote option and never
caught up with tari.mode: remote (#103, v1.14.0). hardware.md never mentioned
tari.mode at all, and stated flatly that "remote-node mode still runs Tari" —
true of the Monero half only. Tari is the largest line item in the budget the
code actually enforces (disk_component_gib: 200 GiB, against 120 pruned /
320 full for Monero), so the one mode that helps a small host most was the one
the sizing page omitted.

hardware.md gains a "Running a node elsewhere" section with the per-mode disk
matrix derived from disk_component_gib, plus what a remote node changes for
RAM, initial sync, egress and onions. The rest of the doc set gets the same
qualifier where it gave sizing or "what runs here" advice.

Corrections found while checking every claim against the code:

- hardware.md recommended a 240 GB SSD for a config whose own budget is
  ~330 GB — setup would warn on the machine the docs suggest.
- The setup/doctor pre-flight note claimed both checks scale with config. The
  disk budget does; the 16 GB RAM warning is unconditional. doctor's memory
  check is a different check (HugePages + MemAvailable), not a repeat.
- P2Pool's "--light-mode saves ~2 GB" describes a knob the stack never passes.
- On an 8 GB host, --skip-optimize also redefines Tari's auto ceiling to ~75%
  of RAM (6 GB of 8) — the escape hatch quietly hands the box to Tari.
- SECURITY.md still said cosign.pub was uncommitted and every release
  unsigned; it is committed and ships in the bundle.
- SECURITY.md's "Tor for all node networking" ignored both remote modes and
  the clearnet initial sync.
- architecture.md claimed every service runs non-root; caddy and the two
  Docker socket proxies run as uid 0 (pinned by the integration suite).
- privacy.md's "only LAN-facing port is stratum" omitted the host-networked
  Caddy on :443, and the Tari DNS resolvers are real public DoT servers that
  are never queried, not "a dead address".
- The remote-node example used a public hostname, but the Tor-egress firewall
  drops everything outside RFC1918 + CGNAT — a public node cannot work by
  default. Those rules live in DOCKER-USER, so they cover the bridged
  containers only; the dashboard's own gRPC poll of a remote Tari node is not
  behind them.
- The control runner dispatches seven actions, not the documented three.
- operations.md's command table omitted three commands `help` prints.
- configuration.md claimed ~113 keys; config.reference.json has 92 leaves
  across 13 sections under the doc's own exclusion rule.
- Data dirs are chowned to a fixed uid 1000, not "your user".
- workers.md still showed the dashboard.workers[] key removed in v1.9.

Verified with make lint (full suite) and an internal link/anchor check.
The adversarial verify stage confirmed a further batch after the first commit
landed. Each re-derived from source:

- configuration.md documented the control channel's password refusal but not
  its second one: `dashboard.control.enabled: true` on a published onion with
  `client_auth: false` is refused outright — and the client_auth row offers
  exactly that setting as a supported choice, so the operator meets a hard
  apply failure the docs never predicted.
- SECURITY.md told reporters to run an up-to-date *checkout*; the documented
  install is a release bundle. Now `./pithead version` / `./pithead upgrade`.
- dashboard.md's badge table omitted the two disk badges the same code path
  emits (`Disk N% full` at 85%, `⚠ Disk N% full` at 95%) — the one warning a
  full disk gives before it corrupts monerod's DB mid-write.
- dashboard.md's top-bar list claimed total hashrate; the header renders the
  last-update time, and total hashrate is documented one section down.
- privacy.md claimed to list every off-box connection but had no row for the
  XvB warm-standby pull. Added with its real routing rule: Tor unless the
  source is a provably-private IP literal.
- README's doc index still said "remote Monero nodes" only.

Verified with make lint (full suite).
Third pass, clearing every confirmed finding that the first two left open.
Each re-derived from source:

- SECURITY.md repeated architecture.md's since-corrected claim that every
  service runs non-root. Caddy and the two Docker socket proxies run as uid 0;
  the integration suite pins exactly that.
- configuration.md called the core group "the ones setup asks about", but
  `workers.list` is in config.core-keys.json and the wizard never prompts for
  it.
- getting-started.md described setup as two stages; it asks a third thing
  afterwards — the dashboard hostname.
- dashboard.md said the Configuration button "then appears" once the channel
  is enabled; it renders unconditionally and explains itself when off.
- dashboard.md sent readers to Worker Inspect to edit `workers.list[]`. Worker
  Inspect writes only the rig's own keys (WORKER_WRITABLE_KEYS), and the host
  gate refuses `workers.list` in either edit mode because it carries each
  rig's host and token. That edit is host-CLI only.
- dashboard.md documented the security card's hour/day/month grouping; that
  became a page-size control (5/10/20/50/100) with range presets and search
  doing the time navigation.
- telegram.md's alert table omitted the high-reject-rate and payout
  found/confirmed alerts, both of which fire and both of which the per-event
  toggle table further down already lists.

Verified with make lint (full suite) and an internal link/anchor check.
Two more instances of the claim already corrected in dashboard.md: the
hour/day/month grouping was replaced by range presets, date fields and search,
with paging doing what grouping used to. SECURITY.md and operations.md both
still described the old control.

Verified with make lint (full suite).
A sweep for each error class already fixed turned up more instances of the
same claims elsewhere:

- faq.md asserted onion addresses for Monero, Tari and P2Pool in two more
  places; only P2Pool's is unconditional, the node onions exist while that
  node runs here.
- architecture.md's opening still said all node P2P and tx traffic routes over
  Tor, which a remote node's direct leg contradicts.
- README's Tor bullet named the clearnet initial sync as the one exception; a
  remote node is a second.
- README's doc index omitted the nine-on-a-default-install caveat that
  docs/README.md already carries.
- configuration.md's stratum_bind row still treated 3333 as fixed rather than
  p2pool.stratum_port's default.

CHANGELOG's hour/day/month mention is left alone on purpose: it records what
shipped at the time.

Verified with make lint (full suite).
Matches the v2 branch: architecture.md's opt-in wallet rows now carry
`wallet-rpc` and `tari-wallet`, so the service list greps cleanly against
docker-compose.yml and against operations.md's logs list.

Verified with make lint (full suite).
…-node-docs-e62736

docs: correct the remote-node story across the doc set, hardware first
11 conflicted files, mostly the same doc pass (PR #1034 on develop, #1035 on
develop-v2) landing twice with small wording drift. Every resolution below was
checked against the code, not against which branch it came from.

- Makefile (test-patch-coverage): took develop's `bash scripts/patch-coverage.sh`
  — closes the vacuous pass where diff-cover exits 0 on "No lines with coverage
  information" (#1000). scripts/patch-coverage.sh exists in the merged tree.
  Reproduces the known-good resolution from the earlier attempt at this merge.

- build/dashboard/mining_dashboard/web/server.py: UNION. Kept v2's
  handle_control_backup/handle_backup_download (#908) and develop's
  _RECORDABLE_WORKER_STATUSES + change_type param on _record_worker_result
  (#1014), dropping v2's now-superseded 4-arg _record_worker_result definition.
  Verified: no duplicate defs, change_type defaults to "apply" so the 4-arg
  worker-apply caller at server.py:353 still works, and the 5-arg
  change_type="upgrade" caller at server.py:373 (already auto-merged in from
  develop) resolves correctly. bash -n / ast.parse both clean.

- README.md / docs/configuration.md ("14 sections" vs "13 sections"): kept
  v2's "14". config.reference.json (unchanged by this merge, so it stays v2's
  copy) has 14 top-level sections once `_docs` and the deprecated
  `xmrig_proxy` alias are excluded — counted directly against the file, and
  it includes `ssh` which only exists on v2's schema.

- SECURITY.md (upgrade guidance): kept v2's version — it names both
  `./pithead upgrade` (Compose) and `./pithead os-update` (appliance); grepped
  pithead and confirmed `os-update` is a real subcommand (pithead:2799,
  PITHEAD_COMMANDS list at pithead:8417). develop's text only mentioned
  `upgrade`, which is incomplete on this branch.

- docker-compose.yml (caddy service): UNION. v2's `PITHEAD_TLS_DIR` volume
  mount and develop's admin-API healthcheck are unrelated additions to the
  same service block; kept both, validated with `yaml.safe_load`.

- docs/dashboard.md and docs/faq.md (Tari-outage rejection behavior): kept
  v2's claim that a Tari outage never rejects workers regardless of
  `dashboard.tari_required`. Verified against
  build/dashboard/mining_dashboard/config/config.py:471-473 and
  service/data_service.py:727-754 — `_apply_worker_rejection` takes only
  `monero_down`, and readmission reads `self.monero_health.healthy` alone;
  `tari_down` (data_service.py:1517) is computed but never passed in. develop's
  text claimed a remote Tari node's reachability gates rejection via
  `tari_required`, which the code does not do.

- docs/dev/testing-strategy.md (tier-1 table, section C): UNION. Kept
  develop's new #972 "Monero reachable but out of sync" row (genuinely new
  content — the `monero_sync_stale` debounce it describes exists in the merged
  data_service.py) and v2's "double outage; readmit follows monerod alone" row
  over develop's competing "readmit only when both healthy" row, since the
  latter contradicts the _apply_worker_rejection reading above.

- docs/operations.md (4 hunks): (1) doctor row — kept develop's fuller text
  adding the `check_monerod_synchronized` check (pithead:4118, called from
  doctor at pithead:1274) that v2's row omitted. (2)+(3) UNION — kept v2's
  render/support-bundle/config-reset/uninstall rows and its whole
  "Appliance-only commands" section; develop's side was simply empty for both
  (genuinely new content, not a competing claim) — confirmed each command
  exists in PITHEAD_COMMANDS and its case arm. (4) remote-node doctor
  behavior — kept develop's fuller sentence (a superset of v2's, adding that
  the Monero sync check skips with no local monerod, consistent with
  check_monerod_synchronized's `container_is_running monerod || return 0`
  early-out).

- pithead (control_upgrade): UNION. Kept v2's `is_appliance` refusal (an
  appliance updates via signed OS images, not tarballs) ahead of develop's
  #1023 cosign-precondition guard — both are real, independent preconditions
  for the one-click upgrade path, and ordering the appliance check first
  means an appliance host gets the correct refusal reason instead of a
  cosign complaint. tests/stack/run.sh's `is_appliance` and #1023 cosign
  fixtures don't set PITHEAD_APPLIANCE, so is_appliance() falls through to
  its /etc/rauc probe (false in the test sandbox) and the existing cosign
  assertions still exercise the same path.

Gates: no conflict markers repo-wide; `bash -n pithead` clean; `make lint`
exit 0; `bash tests/stack/run.sh` 2333 passed/0 failed; `make test-frontend`
375 passed/0 failed; `make test-dashboard` 1818 passed/0 failed, 96.79% total
coverage (gate is >=80%).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@VijitSingh97
VijitSingh97 merged commit 3c02296 into develop-v2 Aug 16, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant