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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.5.2
1.5.3
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,11 @@ function DashboardView({
</div>
</div>
<${AdvancedHint} ui=${ui} onView=${onView} onDismissHint=${onDismissHint} />
${configView ? html`<${ConfigView} /><${SecurityPanel} />` : null}
${
configView
? html`<div class="card-stack"><${ConfigView} /><${SecurityPanel} /></div>`
: null
}
${
configView
? null
Expand Down
15 changes: 10 additions & 5 deletions build/dashboard/mining_dashboard/web/static/configview.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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`<div class="config-modal-backdrop">
<div class="card config-modal">
Expand All @@ -76,8 +79,10 @@ const PreviewModal = ({ preview, confirmText, onConfirmText, onConfirm, onCancel
? html`<p class="text-muted">No configuration changes detected.</p>`
: html`<ul class="config-preview-list">
${changes.map(
(c) => html`<li class=${c.flag === "DEST" ? "config-preview-dest" : ""}>
${c.flag === "DEST" ? "⚠ " : ""}${c.msg}</li>`,
(
c,
) => html`<li class=${c.flag === "DEST" ? "config-preview-dest" : c.flag === "HOST" ? "config-preview-host" : ""}>
${c.flag === "DEST" ? "⚠ " : c.flag === "HOST" ? "ℹ " : ""}${c.msg}</li>`,
)}
</ul>`
}
Expand All @@ -91,7 +96,7 @@ const PreviewModal = ({ preview, confirmText, onConfirmText, onConfirm, onCancel
<div class="config-modal-actions">
<button class="btn-toggle" onClick=${onCancel} disabled=${busy}>Cancel</button>
<button class="btn-toggle active" onClick=${onConfirm}
disabled=${busy || changes.length === 0 || !armed}>
disabled=${busy || committable.length === 0 || !armed}>
${busy ? "Applying…" : "Confirm & apply"}
</button>
</div>
Expand Down Expand Up @@ -190,8 +195,8 @@ export class ConfigView extends Component {
if (phase === "disabled") {
return html`<div class="card">
<h3>Configuration</h3>
<p>Configuration editing is off (the default). To enable it, set
<code>dashboard.control.enabled: true</code> in <code>config.json</code> on the host and
<p>Configuration editing is off (the default). To enable it, set <code>dashboard.control.enabled: true</code>
in <code>config.json</code> on the host and
run <code>./pithead apply</code>. It requires a dashboard login.</p>
</div>`;
}
Expand Down
13 changes: 13 additions & 0 deletions build/dashboard/mining_dashboard/web/static/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion build/dashboard/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
27 changes: 27 additions & 0 deletions build/dashboard/tests/service/test_control_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
2 changes: 1 addition & 1 deletion build/dashboard/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions pithead
Original file line number Diff line number Diff line change
Expand Up @@ -4743,6 +4743,13 @@ control_preview() { # <request-file> <id> <actor> <control-dir>
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
Expand Down