diff --git a/CHANGELOG.md b/CHANGELOG.md index e84aaf87..0c021862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,22 @@ per the process in [`docs/releasing.md`](docs/releasing.md). ## [Unreleased] +## [1.5.3] - 2026-07-14 + +### Fixed + +- **Config editor: a clear note for host-only blocks instead of a silent "no changes" (#519).** + Editing `dashboard.energy` — read straight from `config.json` and never rendered to `.env` — made + the editor report "No configuration changes detected" and disable Apply, a silent no-op that looked + broken. The preview now surfaces a non-committable note that the block is applied on the host. + (Committing `dashboard.energy` from the dashboard is tracked separately in #504.) +- **Configuration tab spacing (#505).** Fixed the run-together "setdashboard.control.enabled" in the + disabled-editing hint, and added a gap between the Configuration editor and the Access-log panel. + +### Testing + +- Guard against drift in the worker writable-key allowlist across its pithead-repo copies (#515). + ## [1.5.2] - 2026-07-14 ### Fixed diff --git a/VERSION b/VERSION index 4cda8f19..8af85beb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.5.2 +1.5.3 diff --git a/build/dashboard/mining_dashboard/web/static/components.mjs b/build/dashboard/mining_dashboard/web/static/components.mjs index b0a951e1..05090cf8 100644 --- a/build/dashboard/mining_dashboard/web/static/components.mjs +++ b/build/dashboard/mining_dashboard/web/static/components.mjs @@ -843,7 +843,11 @@ function DashboardView({ <${AdvancedHint} ui=${ui} onView=${onView} onDismissHint=${onDismissHint} /> - ${configView ? html`<${ConfigView} /><${SecurityPanel} />` : null} + ${ + configView + ? html`
<${ConfigView} /><${SecurityPanel} />
` + : null + } ${ configView ? null diff --git a/build/dashboard/mining_dashboard/web/static/configview.mjs b/build/dashboard/mining_dashboard/web/static/configview.mjs index 2e997d90..cad68894 100644 --- a/build/dashboard/mining_dashboard/web/static/configview.mjs +++ b/build/dashboard/mining_dashboard/web/static/configview.mjs @@ -67,6 +67,9 @@ const Field = ({ field, edits, onEdit }) => { const PreviewModal = ({ preview, confirmText, onConfirmText, onConfirm, onCancel, busy }) => { const changes = preview.changes || []; + // #519: HOST-flagged entries (e.g. dashboard.energy) are config.json-only — informational, not + // committable from the dashboard, so they never arm the Apply button. + const committable = changes.filter((c) => c.flag !== "HOST"); const armed = !preview.destructive || confirmText === "APPLY"; return html`
@@ -76,8 +79,10 @@ const PreviewModal = ({ preview, confirmText, onConfirmText, onConfirm, onCancel ? html`

No configuration changes detected.

` : html`` } @@ -91,7 +96,7 @@ const PreviewModal = ({ preview, confirmText, onConfirmText, onConfirm, onCancel
@@ -190,8 +195,8 @@ export class ConfigView extends Component { if (phase === "disabled") { return html`

Configuration

-

Configuration editing is off (the default). To enable it, set - dashboard.control.enabled: true in config.json on the host and +

Configuration editing is off (the default). To enable it, set dashboard.control.enabled: true + in config.json on the host and run ./pithead apply. It requires a dashboard login.

`; } diff --git a/build/dashboard/mining_dashboard/web/static/dashboard.css b/build/dashboard/mining_dashboard/web/static/dashboard.css index 9201e6f3..a16563f2 100644 --- a/build/dashboard/mining_dashboard/web/static/dashboard.css +++ b/build/dashboard/mining_dashboard/web/static/dashboard.css @@ -106,6 +106,14 @@ body { min-width: 0; } +/* Vertically-stacked cards that aren't in a .grid need their own gap (#505: the Configuration + * editor and the Access-log panel otherwise touch with no space between them). */ +.card-stack { + display: flex; + flex-direction: column; + gap: 20px; +} + /* Brand block (Issue #81): the Pithead mark + wordmark in the header, with the host IP demoted * to a subtitle beneath the name. The host IP (HOST_IP) is arbitrary user input; a long unbroken * value (no hyphens/dots to break at) would otherwise push the header — and the page — wider than @@ -1094,6 +1102,11 @@ button.upgrade-btn { .config-preview-dest { color: var(--warn); } +/* #519: a config.json-only block (e.g. dashboard.energy) edited from the dashboard — informational, + * not committable here, so it's dimmed to read as a note rather than a pending change. */ +.config-preview-host { + color: var(--text-muted); +} .config-confirm-type { display: block; margin: 12px 0; diff --git a/build/dashboard/pyproject.toml b/build/dashboard/pyproject.toml index 1b454053..474ced31 100644 --- a/build/dashboard/pyproject.toml +++ b/build/dashboard/pyproject.toml @@ -7,7 +7,7 @@ name = "mining-dashboard" # Keep in lockstep with the top-level VERSION file — the single source of truth for the stack version # (#44). A shell test (tests/stack/run.sh) fails if these drift; the dashboard *displays* the version # from VERSION (baked in as PITHEAD_VERSION, #58), so this is packaging metadata only. -version = "1.5.2" +version = "1.5.3" description = "Monitoring dashboard and XvB switching engine for Pithead" readme = "README.md" requires-python = ">=3.11" diff --git a/build/dashboard/tests/service/test_control_service.py b/build/dashboard/tests/service/test_control_service.py index 08b1f034..d5fc3ce3 100644 --- a/build/dashboard/tests/service/test_control_service.py +++ b/build/dashboard/tests/service/test_control_service.py @@ -218,3 +218,30 @@ def test_submit_worker_apply_spools_tokenless_intent(self, spool): } # No secret / addressing leaks into the container-writable spool. assert "host" not in req and "port" not in req and "token" not in req + + +def test_writable_key_allowlist_has_no_intra_repo_drift(): + """#515: the worker writable-key allowlist is hardcoded in THREE places kept in sync only by + comments — the dashboard (WORKER_WRITABLE_KEYS), the pithead host runner + (control_worker_apply's jq allowlist), and rigforge's control-server.py WRITABLE (the authority + the rig enforces, #236). Drift means edits for the drifted key silently fail closed. Guard the + two pithead-repo copies here; rigforge#236 carries the reciprocal check on the rig side.""" + import re + from pathlib import Path + + canonical = {"pools", "DONATION", "autotune", "watchdog", "watchdog_interval_min", "max_temp_c"} + assert set(control_service.WORKER_WRITABLE_KEYS) == canonical + + # The pithead CLI lives at the repo root; the dashboard-only Docker test image doesn't ship it. + # Verify the cross-file drift where pithead is reachable (full checkout / CI shell tests), and + # skip cleanly where it isn't. + here = Path(__file__).resolve() + pithead_path = next((p / "pithead" for p in here.parents if (p / "pithead").is_file()), None) + if pithead_path is None: + pytest.skip("pithead CLI not present in this test context (dashboard-only image)") + pithead = pithead_path.read_text() + # [^\]]* (not .*?) so the match survives the jq array being reflowed across multiple lines. + m = re.search(r"\(\[([^\]]*)\]\)\s*as\s*\$ok", pithead) + assert m, "could not find the writable-key allowlist in pithead's control_worker_apply" + pithead_keys = set(re.findall(r'"([^"]+)"', m.group(1))) + assert pithead_keys == canonical, f"pithead allowlist {pithead_keys} drifted from {canonical}" diff --git a/build/dashboard/uv.lock b/build/dashboard/uv.lock index 9d7e79c9..a6b73dbf 100644 --- a/build/dashboard/uv.lock +++ b/build/dashboard/uv.lock @@ -772,7 +772,7 @@ wheels = [ [[package]] name = "mining-dashboard" -version = "1.5.2" +version = "1.5.3" source = { editable = "." } dependencies = [ { name = "aiofiles" }, diff --git a/pithead b/pithead index 178d4c11..6d771171 100755 --- a/pithead +++ b/pithead @@ -4743,6 +4743,13 @@ control_preview() { # result=$(printf '%s\n' "$out" | jq -R -s ' [split("\n")[] | select(length > 0) | split("\t") | {flag: .[0], key: .[1], msg: (.[2:] | join("\t"))}] | {status: "previewed", changes: ., destructive: (map(.flag == "DEST") | any), ts: (now | floor)}') + # #519: dashboard.energy is config.json-only (never rendered to .env), so an energy-only edit + # produces no porcelain change and the UI would otherwise show a bare "no changes detected". + # Surface it as a non-committable HOST note so the operator knows to apply it on the host + # (committing this block from the dashboard is tracked in #504). + if ! jq -e --slurpfile live "$CONFIG_FILE" '(.dashboard.energy // {}) == ($live[0].dashboard.energy // {})' "$staged" >/dev/null 2>&1; then + result=$(printf '%s' "$result" | jq '.changes += [{flag:"HOST",key:"dashboard.energy",msg:"dashboard.energy changed — this block is edited directly in config.json on the host: set it and run \"./pithead apply\". Committing it from the dashboard is tracked in #504."}]') + fi control_write_result "$cdir/results" "$id" "$result" control_audit "$cdir/audit/control.log" "$id" "$actor" "preview" "previewed" "$(porcelain_keys "$out")" else