Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion docs/pithead-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion rigforge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() { # <hashrate|"">
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
Expand Down
14 changes: 14 additions & 0 deletions tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down