Skip to content

chore: sync develop into develop-v2 (32 commits) - #1031

Closed
VijitSingh97 wants to merge 33 commits into
develop-v2from
sync/develop-into-v2-0816
Closed

chore: sync develop into develop-v2 (32 commits)#1031
VijitSingh97 wants to merge 33 commits into
develop-v2from
sync/develop-into-v2-0816

Conversation

@VijitSingh97

Copy link
Copy Markdown
Collaborator

Routine sync so the v2 wave PRs open against a current base. 32 commits from develop.

Conflicts, and how each was decided

Five conflicts. None was resolved by preferring a branch — each was decided against the code.

File Resolution
pithead / control_upgrade Union. The appliance refusal (v2) and the cosign precondition (#1023, develop) are independent preconditions. Appliance is checked first: an appliance cannot take a tarball upgrade whatever the host holds, so that is the informative answer.
docker-compose.yml / caddy Union. v2 added the appliance TLS mount, develop added the config-loaded healthcheck (#904). Different keys, both belong.
Makefile / test-patch-coverage Took develop. #1000 replaces the bare diff-cover call with scripts/patch-coverage.sh, closing the vacuous pass where diff-cover exits 0 on "No lines with coverage information". Strictly better; confirmed the script exists in the merged tree.
web/server.py Union. v2 added the backup handlers (#908); develop added _RECORDABLE_WORKER_STATUSES and the change_type parameter (#1014). Verified: no duplicate definitions result, and the new parameter defaults so existing 4-arg callers still work.
docs/dev/testing-strategy.md Kept develop's new #972 row and v2's wording of the double-outage row. Decided by the code, not by preference — data_service readmits on monerod alone, and test_readmit_ignores_tari_state_entirely pins exactly that.

Gates on the merge result

Gate Result
make lint 0 errors; docs voice clean; operator strings clean
tests/stack 2333 passed, 0 failed
dashboard 1818 passed, 0 failed — 96.78% coverage (gate 80%)
frontend 375 passed, 0 failed

Merge first, before the v2 wave PRs, so they open against a current base.

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 3 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)
Routine sync, 32 commits. Five conflicts, all resolved against the code rather than by
preferring a side:

- pithead / control_upgrade: UNION. The appliance refusal (v2) and the cosign precondition
  (#1023, develop) are independent preconditions and both belong. Appliance is checked FIRST:
  an appliance cannot take a tarball upgrade whatever the host holds, so that is the
  informative answer.
- docker-compose.yml / caddy: UNION. v2 added the appliance TLS mount, develop added the
  config-loaded healthcheck (#904).
- Makefile / test-patch-coverage: took develop. #1000 replaces the bare diff-cover call with
  scripts/patch-coverage.sh, which closes the vacuous pass where diff-cover exits 0 on
  "No lines with coverage information".
- web/server.py: UNION. v2 added the backup handlers (#908), develop added
  _RECORDABLE_WORKER_STATUSES and the change_type parameter (#1014). Verified no duplicate
  definition results and that the new parameter defaults, so existing 4-arg callers still work.
- docs/dev/testing-strategy.md: kept develop's new #972 row AND v2's wording of the
  double-outage row. The wording was decided by the code, not by branch preference:
  data_service readmits on monerod alone and test_readmit_ignores_tari_state_entirely pins it.

Gates on the merge result: make lint 0 errors (docs voice + operator strings clean),
stack 2333/0, dashboard 1818/0 at 96.78% coverage, frontend 375/0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@VijitSingh97

Copy link
Copy Markdown
Collaborator Author

Superseded by the fresh sync — this one went stale when #1035 landed on develop-v2.

VijitSingh97 added a commit that referenced this pull request Aug 16, 2026
…v2-0816b

chore: sync develop into develop-v2 (supersedes #1031)
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