Skip to content

Commit 9b0e17e

Browse files
VijitSingh97claude
andcommitted
feat(dashboard): render Worker Inspect enriched stats as a label/value table in the detail view (#507)
The Worker Inspect detail panel showed a rig's RigForge enriched feed as the same horizontal badge row the compact Workers-Alive list uses. In the single-rig detail view a label -> value table reads better. `_rigforge_display` now builds both outputs from one pass so they can't drift: `chips` (unchanged, for the compact list) and `stats` (label/value split, for the detail table). The detail view renders `stats` as a table reusing the existing worker-history styling; warn/bad metrics colour their value. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d4af133 commit 9b0e17e

4 files changed

Lines changed: 177 additions & 66 deletions

File tree

build/dashboard/mining_dashboard/web/static/workerview.mjs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export class WorkerInspect extends Component {
171171
<${InfoCard} label="Hashrate (1m)" value=${detail.hashrate || "—"} />
172172
<${InfoCard} label="RigForge" value=${detail.rigforge ? detail.rigforge.version || "yes" : "—"} />
173173
</div>
174-
${detail.rigforge ? html`<${Chips} chips=${detail.rigforge.chips} />` : null}
174+
${detail.rigforge ? html`<${StatsTable} stats=${detail.rigforge.stats} />` : null}
175175
176176
<h4 class="mt-2">Edit config</h4>
177177
${
@@ -212,10 +212,23 @@ export class WorkerInspect extends Component {
212212
const InfoCard = ({ label, value }) => html`
213213
<div class="stat-card"><h5>${label}</h5><p>${value}</p></div>`;
214214

215-
const Chips = ({ chips }) =>
216-
chips && chips.length
217-
? html`<div class="badge-row mt-1">${chips.map(
218-
(c) =>
219-
html`<span class=${"badge badge-" + c.variant} title=${c.title || ""}>${c.text}</span>`,
220-
)}</div>`
215+
// The compact Workers-Alive list renders the enriched feed as a horizontal badge row; here in the
216+
// single-rig detail view the same server-built metrics read better as a label → value table (#507).
217+
// `stats` is the {label, value, variant, title} split of the very chips the list uses. A warn/bad
218+
// variant (bad governor, throttling, thermal hold) colours its value; `outline` metrics stay plain.
219+
const STAT_VALUE_CLS = { ok: "status-ok", warn: "status-warn", bad: "status-bad" };
220+
export const StatsTable = ({ stats }) =>
221+
stats && stats.length
222+
? html`
223+
<div class="table-scroll mt-1">
224+
<table class="worker-history">
225+
<tbody>${stats.map(
226+
(s) => html`
227+
<tr>
228+
<td class="text-muted" title=${s.title || ""}>${s.label}</td>
229+
<td class=${STAT_VALUE_CLS[s.variant] || ""}>${s.value}</td>
230+
</tr>`,
231+
)}</tbody>
232+
</table>
233+
</div>`
221234
: null;

build/dashboard/mining_dashboard/web/views.py

Lines changed: 72 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -614,52 +614,60 @@ def _fmt_num(v):
614614

615615

616616
def _rigforge_display(rf):
617-
"""A ``{version, miner_down, chips}`` view of a worker's parsed ``rigforge`` block, or ``None``
618-
for a plain-xmrig worker (#235). Each chip is ``{text, variant, title}`` (the same shape the
619-
Badges component renders) and is emitted ONLY when its data is present — a rig with no RAPL
620-
shows no power chip, a disabled watchdog shows no watchdog chip. Building the chip set (and its
621-
thresholds) here keeps the client a dumb renderer, matching the ``_reject_flag`` precedent."""
617+
"""A ``{version, miner_down, chips, stats}`` view of a worker's parsed ``rigforge`` block, or
618+
``None`` for a plain-xmrig worker (#235). Each metric is emitted ONLY when its data is present —
619+
a rig with no RAPL shows no power row, a disabled watchdog shows no watchdog row.
620+
621+
Both outputs come from one pass so they can't drift: ``chips`` is the merged ``{text, variant,
622+
title}`` badge shape the compact Workers-Alive list renders, and ``stats`` is the same metrics
623+
split into ``{label, value, variant, title}`` for the Worker Inspect detail table (#507).
624+
Building the set (and its thresholds) here keeps the client a dumb renderer, matching the
625+
``_reject_flag`` precedent."""
622626
if not rf:
623627
return None
624-
chips = []
628+
rows = []
629+
630+
def add(label, value, chip, variant, title):
631+
rows.append(
632+
{"label": label, "value": value, "chip": chip, "variant": variant, "title": title}
633+
)
634+
625635
if rf.get("miner_down"):
626-
chips.append(
627-
{
628-
"text": "miner down",
629-
"variant": "bad",
630-
"title": "RigForge is up but its XMRig API is unreachable — the rig is present but "
631-
"not mining. Live hashrate and uptime come from the proxy.",
632-
}
636+
add(
637+
"Miner",
638+
"down",
639+
"miner down",
640+
"bad",
641+
"RigForge is up but its XMRig API is unreachable — the rig is present but not mining. "
642+
"Live hashrate and uptime come from the proxy.",
633643
)
634644

635645
health = rf.get("health") or {}
636646
if health.get("throttling") is True:
637-
chips.append(
638-
{"text": "throttling", "variant": "bad", "title": "CPU is thermal/power throttling."}
639-
)
647+
add("CPU", "throttling", "throttling", "bad", "CPU is thermal/power throttling.")
640648
gov = health.get("governor")
641649
if gov:
642650
ok = gov == "performance"
643-
chips.append(
644-
{
645-
"text": f"gov: {gov}",
646-
"variant": "ok" if ok else "warn",
647-
"title": "CPU frequency governor"
648-
+ ("" if ok else " — 'performance' is recommended for mining."),
649-
}
651+
add(
652+
"Governor",
653+
gov,
654+
f"gov: {gov}",
655+
"ok" if ok else "warn",
656+
"CPU frequency governor"
657+
+ ("" if ok else " — 'performance' is recommended for mining."),
650658
)
651659
hp = _num(health.get("hugepages_total"))
652660
if hp is not None:
653-
chips.append(
654-
{
655-
"text": f"HP {_fmt_num(hp)}",
656-
"variant": "outline",
657-
"title": f"HugePages allocated: {_fmt_num(hp)}.",
658-
}
661+
add(
662+
"HugePages",
663+
_fmt_num(hp),
664+
f"HP {_fmt_num(hp)}",
665+
"outline",
666+
f"HugePages allocated: {_fmt_num(hp)}.",
659667
)
660668
board = health.get("board")
661669
if board:
662-
chips.append({"text": board, "variant": "outline", "title": "Mainboard (firmware)."})
670+
add("Mainboard", board, board, "outline", "Mainboard (firmware).")
663671

664672
power = rf.get("power") or {}
665673
watts = _num(power.get("watts"))
@@ -670,49 +678,56 @@ def _rigforge_display(rf):
670678
parts.append(f"{_fmt_num(round(watts, 1))} W")
671679
if hspw is not None:
672680
parts.append(f"{_fmt_num(round(hspw, 1))} H/s·W")
673-
chips.append(
674-
{"text": " · ".join(parts), "variant": "outline", "title": "Power draw / efficiency."}
675-
)
681+
text = " · ".join(parts)
682+
add("Power / efficiency", text, text, "outline", "Power draw / efficiency.")
676683

677684
tune = rf.get("tune") or {}
678685
if tune.get("target"):
679-
chips.append(
680-
{
681-
"text": f"tune: {tune['target']}",
682-
"variant": "outline",
683-
"title": "Active tuning target.",
684-
}
686+
add(
687+
"Tuning target",
688+
tune["target"],
689+
f"tune: {tune['target']}",
690+
"outline",
691+
"Active tuning target.",
685692
)
686693
if tune.get("autotune_enabled") and tune.get("autotune_next"):
687-
chips.append(
688-
{
689-
"text": f"autotune → {tune['autotune_next']}",
690-
"variant": "outline",
691-
"title": "Next scheduled autotune run.",
692-
}
694+
add(
695+
"Autotune",
696+
tune["autotune_next"],
697+
f"autotune → {tune['autotune_next']}",
698+
"outline",
699+
"Next scheduled autotune run.",
693700
)
694701

695702
wd = rf.get("watchdog") or {}
696703
if wd.get("enabled"):
697704
temp = _num(wd.get("temp_c"))
698705
maxt = _num(wd.get("max_temp_c"))
699706
if wd.get("thermal_hold") is True:
700-
chips.append(
701-
{
702-
"text": "thermal hold",
703-
"variant": "bad",
704-
"title": "Watchdog is holding the rig back — temperature above its ceiling.",
705-
}
707+
add(
708+
"Watchdog",
709+
"thermal hold",
710+
"thermal hold",
711+
"bad",
712+
"Watchdog is holding the rig back — temperature above its ceiling.",
706713
)
707714
elif temp is not None:
708-
label = f"{_fmt_num(round(temp, 1))}°C"
715+
text = f"{_fmt_num(round(temp, 1))}°C"
709716
if maxt is not None:
710-
label += f" / {_fmt_num(maxt)}°C"
711-
chips.append(
712-
{"text": label, "variant": "outline", "title": "Watchdog temperature / ceiling."}
713-
)
717+
text += f" / {_fmt_num(maxt)}°C"
718+
add("Temp / max", text, text, "outline", "Watchdog temperature / ceiling.")
714719

715-
return {"version": rf.get("version"), "miner_down": bool(rf.get("miner_down")), "chips": chips}
720+
chips = [{"text": r["chip"], "variant": r["variant"], "title": r["title"]} for r in rows]
721+
stats = [
722+
{"label": r["label"], "value": r["value"], "variant": r["variant"], "title": r["title"]}
723+
for r in rows
724+
]
725+
return {
726+
"version": rf.get("version"),
727+
"miner_down": bool(rf.get("miner_down")),
728+
"chips": chips,
729+
"stats": stats,
730+
}
716731

717732

718733
def build_system(data):

build/dashboard/tests/frontend/components.test.mjs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import assert from 'node:assert/strict';
1313
import { readFileSync } from 'node:fs';
1414

1515
import { App } from '../../mining_dashboard/web/static/components.mjs';
16+
import { StatsTable } from '../../mining_dashboard/web/static/workerview.mjs';
1617
import { render } from './helpers/render.mjs';
1718

1819
const BASE = JSON.parse(readFileSync(new URL('./fixtures/state.json', import.meta.url)));
@@ -565,3 +566,26 @@ test('no WorkerInspect overlay when none is selected (#185)', () => {
565566
s.control_enabled = true;
566567
assert.doesNotMatch(renderApp({ state: s }), /worker-inspect-overlay/);
567568
});
569+
570+
test('StatsTable renders the enriched feed as a label/value table, colouring warn/bad values (#507)', () => {
571+
// The detail view swaps the compact list's badge row for a table: label cell + value cell,
572+
// driven by the server-built {label, value, variant, title} stats. warn/bad colour the value.
573+
const html = render(StatsTable, {
574+
stats: [
575+
{ label: 'Governor', value: 'powersave', variant: 'warn', title: 'CPU governor' },
576+
{ label: 'HugePages', value: '1280', variant: 'outline', title: '' },
577+
{ label: 'CPU', value: 'throttling', variant: 'bad', title: 'hot' },
578+
],
579+
});
580+
assert.match(html, /class="worker-history"/); // reuses the existing detail-table styling
581+
assert.doesNotMatch(html, /badge-row/); // NOT the compact list's badge row
582+
assert.match(html, /Governor<\/td>/);
583+
assert.match(html, /class="status-warn">powersave/); // warn colours its value
584+
assert.match(html, /class="status-bad">throttling/); // bad colours its value
585+
assert.match(html, /<td class="">1280/); // outline metric stays plain
586+
});
587+
588+
test('StatsTable renders nothing when a rig reports no metrics (#507)', () => {
589+
assert.equal(render(StatsTable, { stats: [] }), '');
590+
assert.equal(render(StatsTable, { stats: undefined }), '');
591+
});

build/dashboard/tests/web/test_views.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -958,12 +958,17 @@ def test_flags_all_rejects_at_floor(self):
958958

959959

960960
class TestRigForgeDisplay:
961-
"""The RigForge enriched-feed chip builder (#235). Parsed block in → {version, chips} out; each
962-
chip emitted only when its data is present, so nothing renders for a plain-xmrig worker."""
961+
"""The RigForge enriched-feed builder (#235). Parsed block in → {version, chips, stats} out;
962+
each metric emitted only when its data is present, so nothing renders for a plain-xmrig worker.
963+
``chips`` feeds the compact badge row; ``stats`` is the same metrics split into label/value for
964+
the Worker Inspect detail table (#507). Both come from one pass, so they stay row-for-row."""
963965

964966
def _chip_texts(self, disp):
965967
return [c["text"] for c in disp["chips"]]
966968

969+
def _stats(self, disp):
970+
return {s["label"]: s for s in disp["stats"]}
971+
967972
def test_none_for_plain_xmrig(self):
968973
assert _rigforge_display(None) is None
969974

@@ -1002,6 +1007,60 @@ def test_full_block_emits_version_and_chips(self):
10021007
# Nothing alarming here: no bad-variant chips.
10031008
assert all(c["variant"] != "bad" for c in disp["chips"])
10041009

1010+
# The detail table (#507) carries the same metrics as label/value pairs, row-for-row with
1011+
# the chips, so the two renderers can't drift.
1012+
assert len(disp["stats"]) == len(disp["chips"])
1013+
stats = self._stats(disp)
1014+
assert stats["Governor"]["value"] == "performance"
1015+
assert stats["Governor"]["variant"] == "ok"
1016+
assert stats["HugePages"]["value"] == "1280"
1017+
assert stats["Mainboard"]["value"] == "ProArt X670E"
1018+
assert stats["Power / efficiency"]["value"] == "142 W · 86.9 H/s·W"
1019+
assert stats["Tuning target"]["value"] == "perf"
1020+
assert stats["Autotune"]["value"] == "Sun 03:00"
1021+
assert stats["Temp / max"]["value"] == "62°C / 85°C"
1022+
1023+
def test_stats_split_label_from_value_and_colour_warn_states(self):
1024+
# The label/value split powers the detail table; a bad/warn metric colours its own value.
1025+
disp = _rigforge_display(
1026+
{
1027+
"version": "1.7.0",
1028+
"miner_down": True,
1029+
"power": {"watts": None, "hs_per_watt": None},
1030+
"tune": {"target": None, "autotune_enabled": False, "autotune_next": None},
1031+
"health": {
1032+
"governor": "powersave",
1033+
"throttling": True,
1034+
"board": None,
1035+
"hugepages_total": None,
1036+
},
1037+
"watchdog": {"enabled": False},
1038+
}
1039+
)
1040+
stats = self._stats(disp)
1041+
assert stats["Miner"]["value"] == "down" and stats["Miner"]["variant"] == "bad"
1042+
assert stats["CPU"]["value"] == "throttling" and stats["CPU"]["variant"] == "bad"
1043+
assert stats["Governor"]["value"] == "powersave"
1044+
assert stats["Governor"]["variant"] == "warn"
1045+
1046+
def test_stats_empty_when_no_metrics_present(self):
1047+
disp = _rigforge_display(
1048+
{
1049+
"version": None,
1050+
"miner_down": False,
1051+
"power": {"watts": None, "hs_per_watt": None},
1052+
"tune": {"target": None, "autotune_enabled": False, "autotune_next": None},
1053+
"health": {
1054+
"governor": None,
1055+
"throttling": None,
1056+
"board": None,
1057+
"hugepages_total": None,
1058+
},
1059+
"watchdog": {"enabled": False, "thermal_hold": None, "temp_c": None},
1060+
}
1061+
)
1062+
assert disp["stats"] == []
1063+
10051064
def test_throttling_and_bad_governor_flag(self):
10061065
disp = _rigforge_display(
10071066
{

0 commit comments

Comments
 (0)