diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ad07a7..1a1a631 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ All notable changes to RigForge are documented here. The format is based on ### Added +- **The enriched feed mirrors the last control outcome (#346).** Pithead's host runner caps its + synchronous `/status` poll after a worker-apply, so a slow auto-rollback outruns it and the change + history row froze at `accepted`. The sister API's `rigforge` block now carries `control` — + `{change_id, status, reason}` read from the control status file — so the next routine poll of the + open read feed catches the terminal outcome without a new authenticated dial to the control port. + A rig that never took a control change, or an unreadable/malformed status file, serves + `control: null`; the mirror never breaks the feed. + - **`doctor` and `apply` now notice when the rig isn't mining at all (#343).** With `pools[0].url` pointed at an unresolvable host, XMRig loops on DNS errors forever — and `doctor` still said "all critical checks passed" while `apply` said "Applied config and restarted". Both now ask the miner diff --git a/docs/operations.md b/docs/operations.md index ce51634..ec34a5f 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -534,8 +534,9 @@ second read-only HTTP endpoint (default `:8081`, keys `api_port`/`api_bind`) wit `:8080` XMRig API has **plus** the data only RigForge knows: applied tune knobs and the last tune run (`/tune`), hashrate-per-watt from RAPL, the doctor probes — HugePages, MSR state, governor, RAM channels/speeds, memory-profile and SMT state, throttling — the watchdog's armed/thermal state, -the effective **writable** config (`config`, pool secrets masked; #253), and a config revision + -last-change provenance (`config_meta`; #254) — as JSON (`/health`, or nested under +the effective **writable** config (`config`, pool secrets masked; #253), a config revision + +last-change provenance (`config_meta`; #254), and the last control-path outcome (`control`, +`{change_id, status, reason}` or `null`; #346) — as JSON (`/health`, or nested under `rigforge` in `/1/summary` and `/2/summary`). It follows XMRig's own architecture: one tiny persistent server (python3 stdlib, ~10 MB idle) ships pre-computed bytes, so a request costs microseconds and cannot touch mining performance; a systemd timer recomputes the state every 15 diff --git a/docs/pithead-integration.md b/docs/pithead-integration.md index a097a40..70c028b 100644 --- a/docs/pithead-integration.md +++ b/docs/pithead-integration.md @@ -82,7 +82,10 @@ pithead#235): config — exactly the control-path allowlist, pool secrets masked; see the prefill note in §3), and `config_meta` (`{revision, changed_at, source, last_change_id}` — `revision` is a content hash of the writable config that changes iff that config changes, so a poller can detect a change made directly - on the rig; `source` is `control`/`local`/`restore`; see §3). + on the rig; `source` is `control`/`local`/`restore`; see §3), and `control` (the last control-path + outcome, `{change_id, status, reason}` mirrored from the control status file, so a poller that + missed a slow rollback catches the terminal outcome here without dialing the control port; `null` + when the rig has never taken a control change or the status file is unreadable). - `GET /health` and `GET /tune` — the `rigforge.health` / `rigforge.tune` objects bare. - When XMRig's own API is unreachable the response is still `200` with `"rigforge": {..., "xmrig_api": "unreachable"}` — a down miner is exactly when the health data diff --git a/rigforge.sh b/rigforge.sh index e01413b..cebb21c 100755 --- a/rigforge.sh +++ b/rigforge.sh @@ -4749,8 +4749,17 @@ _api_config_meta_json() { fi } +# #346: the last control outcome mirrored into the feed as `rigforge.control`. Pithead's host runner +# caps its synchronous /status poll after a worker-apply, so a slow rollback outruns it; rather than a +# new authenticated dial to the control port, the rig mirrors {change_id, status, reason} from the +# status.json _control_status already writes 644, and the next routine poll catches up. Missing, +# unreadable, or malformed status.json -> null — the mirror must never break the feed. +_api_control_json() { + jq -c '{change_id, status, reason}' "${RIGFORGE_CONTROL_STATE:-/var/lib/rigforge-control}/status.json" 2>/dev/null || echo null +} + _api_rigforge_block() { # - jq -n --arg v "$(cat "$SCRIPT_DIR/VERSION" 2>/dev/null || echo unknown)" --arg xv "$XMRIG_VERSION" --arg xc "$XMRIG_COMMIT" --argjson tune "$(_api_tune_json)" --argjson power "$(_api_power_json "$1")" --argjson health "$(_health_json)" --argjson watchdog "$(_watchdog_json)" --argjson config "$(_api_config_json)" --argjson config_meta "$(_api_config_meta_json)" '{version: $v, xmrig_version: $xv, xmrig_commit: $xc, tune: $tune, power: $power, health: $health, watchdog: $watchdog, config: $config, config_meta: $config_meta}' + jq -n --arg v "$(cat "$SCRIPT_DIR/VERSION" 2>/dev/null || echo unknown)" --arg xv "$XMRIG_VERSION" --arg xc "$XMRIG_COMMIT" --argjson tune "$(_api_tune_json)" --argjson power "$(_api_power_json "$1")" --argjson health "$(_health_json)" --argjson watchdog "$(_watchdog_json)" --argjson config "$(_api_config_json)" --argjson config_meta "$(_api_config_meta_json)" --argjson control "$(_api_control_json)" '{version: $v, xmrig_version: $xv, xmrig_commit: $xc, tune: $tune, power: $power, health: $health, watchdog: $watchdog, config: $config, config_meta: $config_meta, control: $control}' } # Produce the sister API's response bodies: compute once, write atomically (tmp + rename, the diff --git a/tests/run.sh b/tests/run.sh index 4fc5b18..9d5b977 100644 --- a/tests/run.sh +++ b/tests/run.sh @@ -6094,6 +6094,20 @@ printf '{broken' >"$APIQ/home/worker/tune-overrides.json" run_refresh assert_eq "corrupt tune-overrides -> applied null, not a crash" "$(jq -r '.applied' "$APIQ/data/tune.json")" "null" rm -f "$APIQ/home/worker/tune-overrides.json" +# #346: the last control outcome mirrored into the feed as rigforge.control — pithead's poller catches +# a late terminal outcome on the open read feed instead of a new authenticated dial to the control port. +# A realistic full status.json (the _control_status shape): the mirror picks exactly the three keys. +CTL346="$(mktemp -d "$SANDBOX/ctl346.XXXXXX")" +printf '%s' '{"status":"rolled_back","change_id":"abc0123456789def","source":"control","applied_at":"2026-01-01T00:00:00Z","changed_keys":["DONATION"],"reason":"miner did not return to a live hashrate; rolled back and live","backup":"/b","warnings":[]}' >"$CTL346/status.json" +run_refresh "RIGFORGE_CONTROL_STATE=$CTL346" +assert_eq "control mirror: exactly {change_id, status, reason} on the feed (#346)" "$(jq -cS '.rigforge.control' "$APIQ/data/summary.json")" '{"change_id":"abc0123456789def","reason":"miner did not return to a live hashrate; rolled back and live","status":"rolled_back"}' +# No status.json (a rig that never took a control change, or control disabled) -> null, feed intact. +run_refresh "RIGFORGE_CONTROL_STATE=$CTL346/absent" +assert_eq "control mirror: no status.json -> null (#346)" "$(jq -c '.rigforge.control' "$APIQ/data/summary.json")" "null" +# Malformed status.json -> null, and the refresh still writes the feed. +printf '{broken' >"$CTL346/status.json" +run_refresh "RIGFORGE_CONTROL_STATE=$CTL346" +assert_eq "control mirror: malformed status.json -> null, refresh survives (#346)" "$(jq -c '.rigforge.control' "$APIQ/data/summary.json")" "null" # #276 (item 5): each `printf | jq ... && mv` (rigforge.sh:4233-4235) is independently atomic — a jq # failure on ONE file must not corrupt or block the others. Break _api_rigforge_block so specifically # health.json's own extraction (`.health + {watchdog: .watchdog}`) fails (health is a string, not an