feat(prometheus-direct-charting): slice 2 — gauge + mean widgets

Add gauge widget (recharts RadialBarChart with configurable threshold
bands, scalar-only per SC-111) and mean widget (client-side average over
range-query window, scalar-only per SC-114). Extract shared _instant_query
helper from the metric path; _fetch_gauge and _fetch_mean dispatch in
PrometheusWidgetSource.fetch(). Both new widget kinds declared in
integrations/prometheus.py and frontend registry.

Backend: 305 pytest pass, ruff clean. Frontend: 136 vitest pass, build+lint green.
This commit is contained in:
Developer
2026-07-08 22:09:10 +00:00
parent 5dad98231f
commit 65bae95e3c
12 changed files with 707 additions and 32 deletions
@@ -113,61 +113,61 @@ This ordering ensures the chart capability is proven against Prometheus before t
**Satisfies:** SC-109, SC-110, SC-111, SC-112, SC-113, SC-114.
- [ ] **2.1 Extract shared `_instant_query` helper + add `_fetch_gauge` to `PrometheusWidgetSource`**
- [x] **2.1 Extract shared `_instant_query` helper + add `_fetch_gauge` to `PrometheusWidgetSource`**
- Files: `backend/src/media_library_viewer_api/widgets/sources.py` (modify)
- Lines: ~40
- Dependencies: Slice 1 (1.3)
- Details: Extract the instant-query HTTP call from the existing `metric` path into a private `_instant_query(base_url, timeout, promql) -> dict` returning `{"result": [...]}` or `{"error": ...}`. Refactor the `metric` path to use it (behavior unchanged). Add `_fetch_gauge(self, base_url, timeout, config)` using `_instant_query`: assert `len(result) == 1` (scalar-only, SC-111); parse `float(result[0]["value"][1])`; return `{"value": float, "warn_at": config.get("warn_at"), "crit_at": config.get("crit_at"), "min": config.get("min"), "max": config.get("max"), "unit": config.get("unit")}`. Dispatch `widget_kind == "gauge"` in `.fetch()`.
- [ ] **2.2 Add `_fetch_mean` to `PrometheusWidgetSource`**
- [x] **2.2 Add `_fetch_mean` to `PrometheusWidgetSource`**
- Files: `backend/src/media_library_viewer_api/widgets/sources.py` (modify)
- Lines: ~30
- Dependencies: 2.1, Slice 1 (1.3 for `_range_query`)
- Details: Add `_fetch_mean(self, base_url, timeout, config)` using the shared `_range_query` from S1. Assert `len(result) == 1` (scalar-only, SC-114). Collect non-null numeric values from the single series; compute arithmetic mean; return `{"value": mean, "unit": config.get("unit")}`. If no numeric samples → `{"error": "..."}`. Dispatch `widget_kind == "mean"` in `.fetch()`.
- [ ] **2.3 Declare `gauge` + `mean` widget kinds in Prometheus integration**
- [x] **2.3 Declare `gauge` + `mean` widget kinds in Prometheus integration**
- Files: `backend/src/media_library_viewer_api/integrations/prometheus.py` (modify)
- Lines: ~25
- Dependencies: 2.1, 2.2
- Details: Add `PrometheusGaugeWidgetConfig` (`promql: str`, `warn_at: float|None`, `crit_at: float|None`, `min: float|None`, `max: float|None`, `unit: str|None`) and `PrometheusMeanWidgetConfig` (`promql: str`, `window: str = "1h"`, `unit: str|None`). Add `widget_kind(...)` entries: `gauge` (refresh 30s), `mean` (refresh 60s).
- [ ] **2.4 Add backend tests for gauge + mean adapters**
- [x] **2.4 Add backend tests for gauge + mean adapters**
- Files: `backend/tests/test_widgets.py` (modify) or `backend/tests/test_prometheus_range.py` (modify)
- Lines: ~60
- Dependencies: 2.1, 2.2
- Details: Mock `requests.get` for gauge: instant query returning 1 series → assert `{value, ...}` shape; returning 2 series → assert `{"error": ...}` (SC-111). Mock for mean: range query returning 1 series with known values `[1.0, 2.0, 3.0]` → assert mean `2.0`; returning 2 series → assert `{"error": ...}` (SC-114). Test timeout/RequestException → `{"error": ...}`.
- [ ] **2.5 Create `PrometheusGaugeWidget` component**
- [x] **2.5 Create `PrometheusGaugeWidget` component**
- Files: `frontend/src/widgets/PrometheusGaugeWidget.tsx` (new)
- Lines: ~90
- Dependencies: Slice 1 (1.5 for widget pattern)
- Details: Render via recharts `RadialBarChart` (no new dep; SC-110). Threshold bands: three stacked `RadialBar` track cells (green `0→warn`, amber `warn→crit`, red `crit→max`) + a value cell. When `warn_at`/`crit_at` absent → single neutral-color track. `min`/`max` default to `0`/`max(value, 1)`. Reuse `SectionCard` + `Alert`/`Skeleton` for loading/error states. Consume `data?.data?.value`, `warn_at`, etc. off `useWidgetData`.
- [ ] **2.6 Create `PrometheusMeanWidget` component**
- [x] **2.6 Create `PrometheusMeanWidget` component**
- Files: `frontend/src/widgets/PrometheusMeanWidget.tsx` (new)
- Lines: ~50
- Dependencies: Slice 1
- Details: Single-value display reusing the `MetricCard` pattern (big number + optional `unit` suffix + subtext "mean over last {window}"). Loading/error/empty via `Skeleton`/`Alert`. No charting library — it's a number (SC-112).
- [ ] **2.7 Create gauge + mean frontend tests**
- [x] **2.7 Create gauge + mean frontend tests**
- Files: `frontend/src/widgets/__tests__/PrometheusGaugeWidget.test.tsx` (new), `frontend/src/widgets/__tests__/PrometheusMeanWidget.test.tsx` (new)
- Lines: ~60
- Dependencies: 2.5, 2.6
- Details: Each covers loading, error, and rendered-data case (SC-125). Gauge test: render with bands (`warn_at`/`crit_at` set) and without (single color). Mean test: render with `value` + `unit`.
- [ ] **2.8 Add gauge + mean bindings to frontend registry**
- [x] **2.8 Add gauge + mean bindings to frontend registry**
- Files: `frontend/src/integrations/registry.ts` (modify)
- Lines: ~35
- Dependencies: 2.5, 2.6
- Details: Import `PrometheusGaugeWidget` + `PrometheusMeanWidget`. Add `gauge` (refresh 30s, configSchema with `promql`, `warn_at`, `crit_at`, `min`, `max`, `unit`) and `mean` (refresh 60s, configSchema with `promql`, `window`, `unit`) entries to the `prometheus` binding's `widgets` array alongside `metric` and `chart`.
- [ ] **2.9 Update widgets barrel + registry tests**
- [x] **2.9 Update widgets barrel + registry tests**
- Files: `frontend/src/widgets/index.ts` (modify), `frontend/src/integrations/registry.test.ts` (modify)
- Lines: ~10
- Dependencies: 2.5, 2.6, 2.8
- Details: Export `PrometheusGaugeWidget` + `PrometheusMeanWidget`. Assert `prometheus` binding has `metric`, `chart`, `gauge`, `mean` (four kinds).
- [ ] **2.10 Verify Slice 2 (build + lint + test)**
- [x] **2.10 Verify Slice 2 (build + lint + test)**
- Run: `cd backend && PYTHONPATH=src pytest tests/test_prometheus_range.py tests/test_widgets.py && cd ../frontend && npm run build && npm run lint`
- Verify: gauge/mean adapter tests pass; frontend typechecks and lints; all four prometheus widget kinds resolve.