diff --git a/audit/internal/platform/sat_fan_stress.go b/audit/internal/platform/sat_fan_stress.go index ccaf30d..5639b30 100644 --- a/audit/internal/platform/sat_fan_stress.go +++ b/audit/internal/platform/sat_fan_stress.go @@ -32,8 +32,8 @@ type FanCheckOptions struct { // FanReading holds one fan sensor reading. type FanReading struct { - Name string - RPM float64 + Name string `json:"name"` + RPM float64 `json:"rpm"` } type cachedPowerReading struct { diff --git a/audit/internal/webui/page_topo_diagram.go b/audit/internal/webui/page_topo_diagram.go index 71bbf22..96eae8f 100644 --- a/audit/internal/webui/page_topo_diagram.go +++ b/audit/internal/webui/page_topo_diagram.go @@ -694,21 +694,59 @@ func renderTopoFanRow(fans []schema.HardwareFanSensor, ceilByName, observedByNam period, glyphSz, glyphSz) + topoFanGlyphPaths() + `` } - fillBar := "" + measured := 0 + fillH := 0.0 if duty >= 0 { - fillBar = fmt.Sprintf(`
`, duty, stroke) + measured = 1 + fillH = duty } + fillBar := fmt.Sprintf(`
`, fillH, stroke) - fmt.Fprintf(&b, `
`+ `%s%s
`, - html.EscapeString(title), side, side, stroke, text, fillBar, glyph) + html.EscapeString(name), int(ceil), measured, html.EscapeString(title), + side, side, stroke, text, fillBar, glyph) } b.WriteString(``) + b.WriteString(topoFanLiveScript()) return b.String() } +// topoFanLiveScript polls the already-collected live-metrics snapshot +// (/api/metrics/latest — served from memory, no BMC call) every 5s and +// updates each fan tile's spin rate, duty fill and tooltip in place. 5s is +// the metrics collector's own sampling period, so polling faster would only +// re-read identical numbers; the endpoint is a mutex read + small JSON, so +// this is cheap even with many viewers. +func topoFanLiveScript() string { + return `` +} + // fanSpinPeriodSec maps an absolute fan RPM to a CSS animation period (one // full turn of the glyph, in seconds). The real period would be 60/RPM — a // blur at any real fan speed — so it is compressed into a band the eye can diff --git a/audit/internal/webui/page_topo_test.go b/audit/internal/webui/page_topo_test.go index acf908e..c0df23f 100644 --- a/audit/internal/webui/page_topo_test.go +++ b/audit/internal/webui/page_topo_test.go @@ -206,6 +206,13 @@ func TestTopoPageRendersCoolingFansAsFlexRow(t *testing.T) { if !strings.Contains(body, "ceiling not measured") { t.Fatalf("topo fan tooltip should note the ceiling is unmeasured: %s", body) } + // Live-update: each tile is addressable and the poll script is present. + if n := strings.Count(body, `class="topo-fan-tile"`); n != 2 { + t.Fatalf("expected 2 addressable fan tiles, got %d", n) + } + if !strings.Contains(body, `data-fan="FAN1"`) || !strings.Contains(body, "/api/metrics/latest") { + t.Fatalf("topo fan row missing live-update wiring: %s", body) + } // Component-detail fallback endpoint must resolve the "fan" type. rec2 := httptest.NewRecorder()