spec(prometheus-direct-charting): verify + close SC-125 + reconcile tracking

Add loading-state tests to the three Prometheus widget test files
(closes SC-125 PARTIAL). Write apply-progress.md, tick all 39 tasks,
add verify-report.md (26/27 PASS, 1 PARTIAL->PASS). All gates green:
293 pytest, ruff clean, npm build+lint 0 errors. No blocking findings.
This commit is contained in:
Developer
2026-07-08 22:50:30 +00:00
parent 67ca0fc3bc
commit 7440603cdb
6 changed files with 442 additions and 19 deletions
@@ -30,6 +30,17 @@ function mockData(data: unknown, error?: string) {
}
describe("PrometheusChartWidget", () => {
it("renders skeleton while loading", () => {
vi.mocked(useWidgets.useWidgetData).mockReturnValue({
data: undefined,
isLoading: true,
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
render(<PrometheusChartWidget widget={widget} refreshIntervalMs={60000} />);
expect(
document.querySelector('[data-slot="skeleton"]'),
).toBeInTheDocument();
});
it("renders a chart with series data", () => {
mockData({
series: [
@@ -30,6 +30,17 @@ function mockData(data: unknown, error?: string) {
}
describe("PrometheusGaugeWidget", () => {
it("renders skeleton while loading", () => {
vi.mocked(useWidgets.useWidgetData).mockReturnValue({
data: undefined,
isLoading: true,
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
render(<PrometheusGaugeWidget widget={widget} refreshIntervalMs={30000} />);
expect(
document.querySelector('[data-slot="skeleton"]'),
).toBeInTheDocument();
});
it("renders a gauge with value and threshold bands", () => {
mockData({
value: 0.75,
@@ -30,6 +30,17 @@ function mockData(data: unknown, error?: string) {
}
describe("PrometheusMeanWidget", () => {
it("renders skeleton while loading", () => {
vi.mocked(useWidgets.useWidgetData).mockReturnValue({
data: undefined,
isLoading: true,
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
render(<PrometheusMeanWidget widget={widget} refreshIntervalMs={60000} />);
expect(
document.querySelector('[data-slot="skeleton"]'),
).toBeInTheDocument();
});
it("renders the mean value with unit", () => {
mockData({ value: 23.5, unit: "%" });
render(<PrometheusMeanWidget widget={widget} refreshIntervalMs={60000} />);
@@ -0,0 +1,54 @@
# Apply Progress: Prometheus Direct Charting
**Change:** `prometheus-direct-charting`
**Phase:** apply-progress
**Date:** 2026-07-08
**Status:** complete — all 39 tasks done, all gates green, verified (see `verify-report.md`)
## Slices delivered
Three slices, each its own commit, each leaving `pytest` / `npm run build` / `npm run lint` / `ruff` green.
### Slice 1 — Prometheus range query + chart rebrand (commit `5dad982`, amended)
- Created `backend/src/media_library_viewer_api/widgets/prometheus_range.py` with `step_for_window(s)` (`max(15, round(s/200))` → ~200 pts/window) and `normalize_prometheus_matrix(result)` (shared label rule extracted from the to-be-removed Grafana path; drops `__name__`/`__*`, joins sorted `k=v`, falls back to `"value"`, dedups with `(n)`).
- Extended `PrometheusWidgetSource.fetch` to dispatch `widget_kind == "chart"``_fetch_chart` hitting `/api/v1/query_range` directly, returning `{series:[...]}` (SC-101..104). Grafana path left intact at this slice.
- Declared `chart` widget kind in `integrations/prometheus.py` (config: `promql`, `window``{1h,6h,24h,7d}`).
- `git mv GrafanaChartWidget.tsx → PrometheusChartWidget.tsx` (recharts body preserved verbatim; empty-state copy updated); `git mv` of its test. Rebound `chart` grafana→prometheus in both registries (SC-105..108).
- Backend tests: new `test_prometheus_range.py` (step + normalization); chart-adapter test in `test_widgets.py`. Frontend registry test updated.
### Slice 2 — Gauge + mean widgets (commit `58be6e0`, amended)
- Extracted shared `_instant_query` helper; added `_fetch_gauge` (instant → scalar, multi-series → `{error}`) and `_fetch_mean` (range query over preset → client-side arithmetic mean of non-null values, scalar-only).
- Declared `gauge` (`promql`, `warn_at`/`crit_at`/`min`/`max`/`unit`) and `mean` (`promql`, `window`, `unit`) kinds in `integrations/prometheus.py`.
- Created `PrometheusGaugeWidget.tsx` (recharts `RadialBarChart`; green/amber/red threshold bands when `warn_at`+`crit_at` set; neutral single track otherwise) and `PrometheusMeanWidget.tsx` (MetricCard-style single value). Wired both into the frontend `prometheus` binding + barrel.
- Tests: backend adapter tests (scalar-only enforcement, mean aggregation incl. NaN-skip, error cases); frontend component tests (error + rendered, gauge with/without bands, mean with/without unit).
### Slice 3 — Grafana removal + config + changelog (commit `ba94317`, amended)
- Deleted `integrations/grafana.py`, `GrafanaLinkWidget.tsx`, `service-tabs/LinksTab.tsx` (+ test). Removed `GrafanaWidgetSource` + adapter registration; `grafana` from `SERVICE_DEFINITIONS`/`SERVICE_ADAPTERS` (BE) and `SERVICE_REGISTRY`/`BUILTIN_WIDGETS` (FE); `get_grafana_status` endpoint; `useGrafanaStatus`/`fetchGrafanaStatus`/`GrafanaStatus`; nav entry; `service-tabs/index.ts` grafana case; `Dashboard.tsx` `OBSERVABILITY_TYPES` grafana member; `ServicesPage.tsx` empty-state copy; grafana tests.
- Rewrote `openspec/config.yaml`: removed stale "Do NOT re-implement charting in-app" + "No recharts/d3" claims; states Manage renders Prometheus-backed metrics directly via recharts and that Grafana is no longer integrated.
- Added `CHANGELOG.md` `[Unreleased]` entry: **BREAKING** — Grafana service type removed; migrate by deleting grafana instances and recreating as Prometheus; `grafana/chart` widgets → `prometheus/chart`.
- Net: **920 lines** across 27 files.
### Coverage close — SC-125 loading-state tests
- Added one `it("renders skeleton while loading")` case to each of the three Prometheus widget test files, asserting the `Skeleton` (`data-slot="skeleton"`) renders under `{ data: undefined, isLoading: true }`. Closes the PARTIAL finding on SC-125.
## Deviations from tasks.md
- None functional. The only textual drift is SC-118: `ObservabilityPage.tsx` had already been refactored into `service-tabs/` (the project map was stale). Removal targets adjusted to the real files (`LinksTab.tsx`, `service-tabs/index.ts`, `navEntries.ts`, `Dashboard.tsx`, `ServicesPage.tsx`); the spec was patched (SC-116/SC-118) to reflect this before apply. SC-118's *intent* (no Grafana UI surface) is fully satisfied.
## Final gate results (re-run after coverage close)
| Gate | Result |
|---|---|
| `backend && PYTHONPATH=src python3 -m pytest -q` | **293 passed**, 2 warnings (pre-existing pythonjsonlogger DeprecationWarning) |
| `backend && PYTHONPATH=src python3 -m ruff check src tests` | **All checks passed** |
| `frontend && npm run build` (`tsc -b` + `vite build`) | **exit 0** (pre-existing chunk-size warning) |
| `frontend && npm run lint` | **0 errors**, 1 pre-existing warning (`WidgetConfigDialog.tsx:370`, untouched) |
| `frontend && npx vitest run` (3 Prom widget tests) | **14 passed** (11 original + 3 new loading) |
## Verification
See `verify-report.md` — adversarial fresh-context review: **26/27 fully PASS, 1 PARTIAL→PASS** (SC-125 closed here). No blocking findings remain.
@@ -183,85 +183,85 @@ This ordering ensures the chart capability is proven against Prometheus before t
> **Spec-text drift note:** SC-118 literally names `ObservabilityPage.tsx`, which was refactored into `service-tabs/`. The *intent* (no Grafana UI surface) is verified by the removals below. `Dashboard.test.tsx` contains "Grafana" as a user-authored shortcut label (unrelated to the grafana service) — SC-116 grep should not flag it; it is left intact.
- [ ] **3.1 Delete backend Grafana integration module**
- [x] **3.1 Delete backend Grafana integration module**
- Files: `backend/src/media_library_viewer_api/integrations/grafana.py` (delete)
- Lines: ~20 (deletion)
- Dependencies: Slice 1 (chart kind already moved to prometheus)
- Details: Delete the file. It contains `GrafanaConfig` + `GrafanaLinkWidgetConfig`.
- [ ] **3.2 Remove Grafana from backend integration registry**
- [x] **3.2 Remove Grafana from backend integration registry**
- Files: `backend/src/media_library_viewer_api/integrations/registry.py` (modify)
- Lines: ~3
- Dependencies: 3.1
- Details: Drop `from ...grafana import DEFINITION as GRAFANA` and the `GRAFANA.service_type: GRAFANA` entry from `SERVICE_DEFINITIONS`.
- [ ] **3.3 Remove `GrafanaWidgetSource` + adapter registration**
- [x] **3.3 Remove `GrafanaWidgetSource` + adapter registration**
- Files: `backend/src/media_library_viewer_api/widgets/sources.py` (modify)
- Lines: ~100 (deletion of `GrafanaWidgetSource` class + `_fetch_chart`)
- Dependencies: Slice 1 (normalization logic already extracted to `prometheus_range.py`)
- Details: Delete the `GrafanaWidgetSource` class entirely (including `_fetch_chart` — its logic was extracted to `normalize_prometheus_matrix` in S1). Remove `"grafana": GrafanaWidgetSource()` from `SERVICE_ADAPTERS`.
- [ ] **3.4 Remove `get_grafana_status` endpoint**
- [x] **3.4 Remove `get_grafana_status` endpoint**
- Files: `backend/src/media_library_viewer_api/routers/monitoring.py` (modify)
- Lines: ~25
- Dependencies: none
- Details: Delete the `@router.get("/grafana-status")` endpoint and its helper. Leave `get_prometheus_status` / `get_alertmanager_status` intact.
- [ ] **3.5 Remove Grafana backend tests**
- [x] **3.5 Remove Grafana backend tests**
- Files: `backend/tests/test_widgets.py` (modify), `backend/tests/test_api.py` (modify), `backend/tests/test_services.py` (modify)
- Lines: ~40
- Dependencies: 3.3
- Details: Delete grafana adapter tests (`test_grafana_adapter_*`), grafana service fixtures, and the `TestGrafanaStatus` test class. SC-120.
- [ ] **3.6 Delete frontend `GrafanaLinkWidget` + barrel export**
- [x] **3.6 Delete frontend `GrafanaLinkWidget` + barrel export**
- Files: `frontend/src/widgets/GrafanaLinkWidget.tsx` (delete), `frontend/src/widgets/index.ts` (modify)
- Lines: ~35
- Dependencies: none
- Details: Delete the file. Remove the `GrafanaLinkWidget` export from `widgets/index.ts`.
- [ ] **3.7 Remove `grafana` binding from frontend registry**
- [x] **3.7 Remove `grafana` binding from frontend registry**
- Files: `frontend/src/integrations/registry.ts` (modify)
- Lines: ~30
- Dependencies: 3.6
- Details: Delete the entire `grafana` key from `SERVICE_REGISTRY`. Drop the `GrafanaLinkWidget` import. SC-117.
- [ ] **3.8 Remove Grafana nav entry**
- [x] **3.8 Remove Grafana nav entry**
- Files: `frontend/src/integrations/navEntries.ts` (modify)
- Lines: ~5
- Dependencies: none
- Details: Drop the `grafana` entry from `SERVICE_TYPE_NAV_ENTRIES` and any now-unused icon import (e.g. `Link2`).
- [ ] **3.9 Remove Grafana status hook + API client function + type**
- [x] **3.9 Remove Grafana status hook + API client function + type**
- Files: `frontend/src/hooks/useObservability.ts` (modify), `frontend/src/api/client.ts` (modify), `frontend/src/types/index.ts` (modify)
- Lines: ~15
- Dependencies: 3.4
- Details: Drop `useGrafanaStatus` + its `fetchGrafanaStatus` import from `useObservability.ts`. Drop `fetchGrafanaStatus` from `client.ts`. Drop `GrafanaStatus` interface from `types/index.ts`.
- [ ] **3.10 Delete Grafana service tab + remove from tab index**
- [x] **3.10 Delete Grafana service tab + remove from tab index**
- Files: `frontend/src/pages/service-tabs/LinksTab.tsx` (delete), `frontend/src/pages/service-tabs/__tests__/LinksTab.test.tsx` (delete), `frontend/src/pages/service-tabs/index.ts` (modify)
- Lines: ~80
- Dependencies: none
- Details: Delete `LinksTab.tsx` (grafana-specific per its docstring) and its test. Remove the `LinksTab` import and `case "grafana":` from `service-tabs/index.ts`.
- [ ] **3.11 Remove Grafana from Dashboard + ServicesPage**
- [x] **3.11 Remove Grafana from Dashboard + ServicesPage**
- Files: `frontend/src/pages/Dashboard.tsx` (modify), `frontend/src/pages/ServicesPage.tsx` (modify)
- Lines: ~5
- Dependencies: none
- Details: Drop `"grafana"` from `OBSERVABILITY_TYPES` set in `Dashboard.tsx`. Update `ServicesPage.tsx` empty-state copy: "Add a Grafana, Prometheus, …" → "Add a Prometheus, …".
- [ ] **3.12 Rewrite `openspec/config.yaml` thin-dashboard rule**
- [x] **3.12 Rewrite `openspec/config.yaml` thin-dashboard rule**
- Files: `openspec/config.yaml` (modify)
- Lines: ~8
- Dependencies: none
- Details: Remove "Do NOT re-implement charting in-app" and "No recharts/d3 is in use" claims. Replace with accurate wording: in-app charting via `recharts` is the sanctioned approach for Prometheus-backed series; Grafana is no longer referenced. SC-121.
- [ ] **3.13 Add CHANGELOG migration note**
- [x] **3.13 Add CHANGELOG migration note**
- Files: `CHANGELOG.md` (modify)
- Lines: ~8
- Dependencies: none
- Details: Add entry under an appropriate version heading: instruct operators to delete existing Grafana service instances and recreate them as Prometheus services; note that `grafana/chart` widgets must be recreated as `prometheus/chart` widgets. SC-122, SC-126.
- [ ] **3.14 Verify Slice 3 (grep-clean + build + lint + test)**
- [x] **3.14 Verify Slice 3 (grep-clean + build + lint + test)**
- Run: `grep -ri grafana backend/src --include='*.py'` → expect zero matches (SC-115)
- Run: `grep -ri grafana frontend/src` → expect zero matches except `Dashboard.test.tsx` shortcut fixture (SC-116)
- Run: `cd backend && PYTHONPATH=src pytest && cd ../frontend && npm run build && npm run lint`
@@ -273,23 +273,23 @@ This ordering ensures the chart capability is proven against Prometheus before t
## Integration and acceptance verification
- [ ] **4.1 Full backend test run**
- [x] **4.1 Full backend test run**
- Run: `cd backend && PYTHONPATH=src pytest`
- Verify: all tests pass; no grafana test references remain; prometheus range/gauge/mean tests pass.
- [ ] **4.2 Full frontend build + lint**
- [x] **4.2 Full frontend build + lint**
- Run: `cd frontend && npm run build && npm run lint`
- Verify: TypeScript compiles; no lint failures; no grafana imports unresolved.
- [ ] **4.3 Grep-clean verification**
- [x] **4.3 Grep-clean verification**
- Run: `grep -ri grafana backend/src --include='*.py'` → zero matches
- Run: `grep -ri grafana frontend/src` → zero matches excluding `Dashboard.test.tsx` shortcut fixture
- Verify: SC-115, SC-116 satisfied.
- [ ] **4.4 config.yaml accuracy check**
- [x] **4.4 config.yaml accuracy check**
- Verify: `openspec/config.yaml` does not contain "Do NOT re-implement charting" or "No recharts/d3"; reflects recharts as sanctioned renderer (SC-121).
- [ ] **4.5 CHANGELOG check**
- [x] **4.5 CHANGELOG check**
- Verify: `CHANGELOG.md` documents the grafana→prometheus migration (SC-122).
---
@@ -0,0 +1,336 @@
# Verify Report — prometheus-direct-charting
> Phase: **verify** · Change: `prometheus-direct-charting` · Repo: `/home/user/manage`
> FRESH-CONTEXT adversarial read-only verification of the change against
> `proposal.md`, `spec.md`, `design.md`, and `tasks.md`. **No source edits.**
> This verify report is the only file written.
**Head commit verified:** `67ca0fc` (`feat(prometheus-direct-charting): slice 3 — remove grafana + config rewrite + changelog`).
Three implementation slices are committed:
- `5dad982` slice 1 — prom range query + chart rebrand
- `65bae95` slice 2 — gauge + mean widgets
- `67ca0fc` slice 3 — grafana removal + config rewrite + changelog
> NOTE: the dispatch brief cited slice hashes `58be6e0` / `ba94317` (an earlier
> amend state). The actual landed commits are `65bae95` / `67ca0fc`. Content of
> all three slices matches the spec/design/tasks; this is informational, not a defect.
---
## 0. Executive summary / verdict
**VERDICT: PASS — implementation complete and green; archive BLOCKED on a
task-hygiene / missing-`apply-progress` issue (reconcilable without code).**
Every functional requirement **SC-101 … SC-127** was checked against source and
**passes**. Grafana is fully excised from the live code paths (service type,
adapters, widgets, hook, API client, type, status endpoint, nav entry, service
tab). Prometheus is the direct chart source: `chart` (multi-series recharts line
chart via `/api/v1/query_range`), `gauge` (instant scalar + threshold bands via
recharts `RadialBarChart`), and `mean` (client-side average) are wired in both
registries with consistent schemas. The shared `normalize_prometheus_matrix` /
`step_for_window` helpers are extracted and unit-tested. All four gates are
green: backend `pytest` (**293 passed**), `ruff` (**clean**), frontend
`npm run build` (**exit 0**), `npm run lint` (**0 errors**).
Findings:
- **[CRITICAL — archive blocker, NOT a code defect]** **19 unchecked
implementation/verification task checkboxes** remain in `tasks.md` (all of
Slice 3 §3.13.14 and Integration §4.14.5), and **`apply-progress.md` does not
exist** to reconcile them. The underlying work *is* done and verified complete
against source; the blocker is that the task tracker was never updated and no
apply-progress artifact was produced. Reconciliation = tick the boxes + write
`apply-progress.md` (no code change). See §4.
- **[WARNING]** SC-125 is **PARTIAL**: none of the three new widget tests
(`PrometheusChartWidget` / `PrometheusGaugeWidget` / `PrometheusMeanWidget`)
exercises an explicit **loading** state (`isLoading:true` → Skeleton). Error
and rendered-data cases exist for all three. The loading branch exists in each
component; the gap is test coverage, not functionality. See §5.
- **[INFO]** Stale generated `.pi-map.md` files still reference Grafana /
`ObservabilityPage.tsx`; these are not deliverable source and are ignored by
> SC-115/116, but should be regenerated (`project_map_patch`/`validate`). The
> `config.yaml` context block also still names `ObservabilityPage.tsx` (refactored
away into `service-tabs/`) — not an SC-121 criterion, minor doc staleness.
- **[INFO]** Working tree is not pristine: an uncommitted cosmetic reformat of
`frontend/src/pages/service-tabs/MediaTab.tsx` (unrelated to this change) plus
untracked `.pi-tmp/*` and `openspec/changes/service-storage-harness/` (the
proposal for the *separate* change SC-127 requires independence from).
---
## 1. Structured status & actionContext findings
The native `gentle-pi.sdd-status` reports `changeName: null` /
`blockedReasons: ["Change selection is ambiguous: …"]` because the engine
auto-detected four active changes. This verify task was **explicitly assigned**
`prometheus-direct-charting`; the ambiguity is a parent-resolution artifact and
does not block this phase.
- `artifactStore: openspec`; change root
`openspec/changes/prometheus-direct-charting/`.
- Artifacts present: `proposal.md`, `spec.md`, `design.md`, `tasks.md`.
- **`apply-progress.md`: MISSING** (confirmed: directory contains only the four
planning docs + this report). This is the root cause of the §4 archive blocker.
- `actionContext`: `mode: repo-local`, `workspaceRoot: /home/user/manage`,
`allowedEditRoots: ["/home/user/manage"]`, `warnings: []`. Implementation
ownership and all target files are provably inside the authoritative workspace. ✓
- The `service-storage-harness` change exists only as an untracked proposal
folder; SC-127 (independence) holds — this change builds/tests green without it.
## 2. Gate results (actual output, run at `67ca0fc`)
| Gate | Command | Result | Evidence |
|------|---------|--------|----------|
| Backend tests | `cd backend && PYTHONPATH=src python3 -m pytest -q` | **PASS** | **293 passed, 2 warnings** in 36.26s. Includes `test_prometheus_range.py` (9) + chart/gauge/mean adapter tests. |
| Backend lint | `cd backend && PYTHONPATH=src python3 -m ruff check src tests` | **PASS** | `All checks passed!` |
| Frontend build | `cd frontend && npm run build` (`tsc -b` + `vite build`) | **PASS** exit 0 | `✓ built in 1.08s`; 2543 modules transformed. Non-fatal `>500 kB` chunk-size warning (pre-existing). |
| Frontend lint | `cd frontend && npm run lint` (`eslint .`) | **PASS** exit 0 | `0 errors, 1 warning`. The warning is `react-hooks/exhaustive-deps` in `WidgetConfigDialog.tsx:370`**pre-existing, untouched by this change** (no slice modified that file). |
### Grep gates (SC-115 / SC-116 / SC-117 / SC-120)
```
grep -rin grafana backend/src --include='*.py'
→ 3 matches, ALL in prometheus_range.py docstrings/comments describing the
extraction/removal (explicitly allowed by SC-115). No live grafana code.
grep -rin grafana frontend/src
→ matches ONLY in: stale generated .pi-map.md / .pi-map.index.md files
(ignored — generated artifacts), and Dashboard.test.tsx lines 108/115
(the whitelisted user-authored shortcut *label* "Grafana"). No live
grafana code, no grafana widget/component/hook.
grep -rin grafana backend/tests --include='*.py'
→ ZERO matches (SC-120). (.pyc cache + .pi-map.md are stale; .py sources clean.)
```
---
## 3. Spec coverage (SC-101 … SC-127)
| SC | Requirement | Verdict | Evidence |
|----|-------------|---------|----------|
| SC-101 | range query → `{series}` shape | **PASS** | `sources.py:_fetch_chart``_range_query` hits `/api/v1/query_range` with `query/start/end/step`; returns `{"series": normalize_prometheus_matrix(matrix)}` = `{label, points:[{t:int, v:float\|null}]}`. Test `test_prometheus_chart_adapter_runs_range_query` asserts endpoint + params + shape. |
| SC-102 | shared, Prometheus-native label normalization | **PASS** | Single helper `normalize_prometheus_matrix` in `prometheus_range.py`; drops `__name__`/`__*`, joins `k=v` (sorted), falls back to `"value"`, dedups with `(n)`. No duplication. `test_prometheus_range.py` covers all rules. |
| SC-103 | range-query errors degrade gracefully | **PASS** | `_range_query`/`_instant_query` catch `asyncio.TimeoutError` + `requests.RequestException``{"error": ...}`; `.fetch()` wraps the whole body in `try/except → {"error"}`. Never raises. Test `test_prometheus_chart_adapter_degrades_on_http_error`. |
| SC-104 | step derived from window (100300 pts) | **PASS** | `step_for_window = max(15, round(window/200))`. Parametrized test asserts 100300 points for `1h/6h/24h/7d` + 15s floor + custom target. |
| SC-105 | `chart` rebind grafana→prometheus (both registries) | **PASS** | Backend `prometheus.py` declares `chart`; frontend `registry.ts` `prometheus` binding has `chart`. Grafana is gone entirely (so trivially offers no `chart`). |
| SC-106 | chart renderer reused unchanged | **PASS** | `PrometheusChartWidget.tsx` preserves `LineChart/Line/XAxis/YAxis/CartesianGrid/Tooltip/ResponsiveContainer/mergeSeries/CHART_COLORS/formatTime`; rename-only + empty-state copy fix. |
| SC-107 | chart multi-series | **PASS** | `series.map((s,i) => <Line dataKey={s.label} .../>)` renders every series; no single-series restriction. |
| SC-108 | chart window is a preset | **PASS** | chart config = `{promql, window}`; `window ∈ {1h,6h,24h,7d}` resolved via `WINDOW_PRESETS` server-side; no raw from/to/step. |
| SC-109 | gauge = instant scalar | **PASS** | `_fetch_gauge` → instant `/api/v1/query` → parses `result[0]["value"][1]``{"value":...}`. Bound to `prometheus` in both registries. |
| SC-110 | gauge threshold bands | **PASS** | `PrometheusGaugeWidget` (recharts `RadialBarChart`) renders green/amber/red track cells when `warn_at`+`crit_at` set, single neutral track otherwise; `min/max` default to `0`/`max(value,1)`. Config accepts `warn_at/crit_at/min/max/unit`. |
| SC-111 | gauge scalar-only | **PASS** | `len(result) != 1 → {"error": "Gauge requires a single-series query; refine your PromQL"}`. Test `test_prometheus_gauge_adapter_rejects_multi_series`. |
| SC-112 | mean = client-side mean over window | **PASS** | `_fetch_mean``_range_query` → averages non-null samples of the single series → `{"value": mean, "unit?"}`. Test asserts mean of `[1,2,3]=2.0`, NaN-skip `[2,NaN,4]=3.0`. |
| SC-113 | mean = plain PromQL + preset | **PASS** | mean config = `{promql, window, unit?}`; user supplies plain PromQL (no `avg_over_time`); window is a preset. |
| SC-114 | mean scalar-only | **PASS** | `len(result) != 1 → {"error": ...}`. Test `test_prometheus_mean_adapter_rejects_multi_series`. |
| SC-115 | no grafana in backend src | **PASS** | grep → only `prometheus_range.py` docstrings (allowed). `integrations/grafana.py` deleted; no `GrafanaWidgetSource`. |
| SC-116 | no grafana in frontend src | **PASS** | grep → only `Dashboard.test.tsx` whitelisted shortcut fixture + stale `.pi-map.md` (ignored). No live code. |
| SC-117 | grafana gone from registries | **PASS** | No `grafana` in backend `SERVICE_DEFINITIONS`/`SERVICE_ADAPTERS` or frontend `SERVICE_REGISTRY`/`BUILTIN_WIDGETS`; `integrations/grafana.py` deleted. `registry.test.ts` asserts 5 service types (no grafana). |
| SC-118 | grafana status + UI removed | **PASS (intent)** | `get_grafana_status` removed (`monitoring.py`); `useGrafanaStatus`/`fetchGrafanaStatus`/`GrafanaStatus` type removed; `LinksTab.tsx`+test deleted; `service-tabs/index.ts` grafana case removed; `navEntries.ts` grafana entry removed; `Dashboard.tsx` `OBSERVABILITY_TYPES` = `{alertmanager, prometheus}`; `ServicesPage.tsx` copy updated. `ObservabilityPage.tsx` was already refactored into `service-tabs/` (spec drift acknowledged in design §0 / spec SC-118 note). |
| SC-119 | orphaned grafana widget degrades | **PASS** | `WidgetInstanceCard`: `resolveWidget``undefined` for a grafana-bound widget (no binding) → renders `"Unknown widget: <kind> (service-bound)"` `Alert`. No crash. |
| SC-120 | grafana tests removed | **PASS** | No grafana in backend test `.py` or frontend widget tests. Only stale `.pyc` cache + `.pi-map.md`. |
| SC-121 | config.yaml accurate | **PASS** | Stale "Do NOT re-implement charting in-app" / "No recharts/d3" claims gone; OBSERVABILITY MODEL states "Manage renders Prometheus-backed metrics directly via recharts … Grafana is no longer integrated." No grafana-as-chart-path. |
| SC-122 | CHANGELOG migration note | **PASS** | `[Unreleased]` has "Direct Prometheus charting" + "**BREAKING** — Grafana service type removed" with migration: delete grafana instances → recreate as Prometheus; `grafana/chart` widgets → `prometheus/chart`. |
| SC-123 | backend tests pass | **PASS** | `pytest` → 293 passed; covers range→`{series}`, gauge scalar-only, mean aggregation, shared label helper. |
| SC-124 | frontend build + lint | **PASS** | `npm run build` exit 0; `npm run lint` 0 errors (1 pre-existing warning). |
| SC-125 | new widget kinds have tests | **PARTIAL** | All three test files exist with **error + rendered** cases (gauge has with/without bands; mean has with/without unit). **No explicit loading-state case** (`isLoading:true`) in any of the three — see §5. |
| SC-126 | no silent data migration | **PASS** | No grafana→prometheus row transformation code anywhere; only docstring references to manual migration. `settings_store.py` migration is unrelated (`jellyseerr→jellyfin`). |
| SC-127 | independent of service-storage-harness | **PASS** | No reference to it in source (only a `prometheus_range.py` docstring noting future reuse); builds/tests green without it; that change exists only as a proposal folder. |
**Functional spec coverage: 26/27 fully PASS, 1 PARTIAL (SC-125, test-coverage only).**
---
## 4. Task completion status — ⚠ archive blocker (reconcilable)
`tasks.md` checkbox state:
- **Slice 1 (§1.11.10): all 10 `[x]`** ✓
- **Slice 2 (§2.12.10): all 10 `[x]`** ✓
- **Slice 3 (§3.13.14): all 14 `[ ]` — UNCHECKED**
- **Integration (§4.14.5): all 5 `[ ]` — UNCHECKED**
Total: 20 checked, **19 unchecked**.
**This is a CRITICAL completeness issue for the archive gate per the verify
contract.** However — and this is the important reconciliation — **the Slice 3
and Integration work is verifiably DONE against source**:
| Unchecked task | Actual state (verified) |
|----------------|-------------------------|
| 3.1 delete `integrations/grafana.py` | **deleted** (`ls` → ENOENT; slice-3 diff `73 ------`) |
| 3.2 drop grafana from backend registry | **done** (`registry.py` has no grafana import/entry) |
| 3.3 remove `GrafanaWidgetSource` + adapter | **done** (`sources.py` has no class; `SERVICE_ADAPTERS` no grafana; diff `113 ---------`) |
| 3.4 remove `get_grafana_status` | **done** (`monitoring.py` grep clean; diff `-25`) |
| 3.5 remove grafana backend tests | **done** (`.py` test grep clean) |
| 3.6 delete `GrafanaLinkWidget` + barrel | **done** (file gone; `index.ts` export dropped) |
| 3.7 remove grafana frontend binding | **done** (`registry.ts` no grafana key; registry.test asserts) |
| 3.8 remove grafana nav entry | **done** (`navEntries.ts` grep clean) |
| 3.9 remove hook + api client + type | **done** (`useObservability.ts`/`client.ts`/`types/index.ts` grep clean) |
| 3.10 delete `LinksTab` + tab-index case | **done** (file + test deleted; `index.ts` grep clean) |
| 3.11 Dashboard + ServicesPage | **done** (`OBSERVABILITY_TYPES` = `{alertmanager,prometheus}`; ServicesPage copy updated) |
| 3.12 rewrite `config.yaml` | **done** (stale claims gone) |
| 3.13 CHANGELOG note | **done** (migration entry present) |
| 3.14 verify slice 3 (grep + build + lint + test) | **done** (all green — §2) |
| 4.1 full backend pytest | **done** (293 passed) |
| 4.2 frontend build + lint | **done** (exit 0 / 0 errors) |
| 4.3 grep-clean | **done** (SC-115/116 satisfied) |
| 4.4 config.yaml check | **done** (SC-121 satisfied) |
| 4.5 CHANGELOG check | **done** (SC-122 satisfied) |
The unchecked boxes are **stale** (work performed, tracker not updated), and
**no `apply-progress.md` exists** to serve as the stale-checkbox reconciliation
record the contract permits. Resolution is a **documentation-only** step:
tick §3.13.14 and §4.14.5, and author `apply-progress.md` describing the three
landed slices. **No code change is required.**
> Per the verify contract, an unchecked implementation-task line is an archive
> blocker until reconciled. Because the implementation is verified complete, this
> blocks **archive** but does **not** block `sdd-sync` of the green code.
---
## 5. TDD compliance & assertion-quality assessment
Strict-TDD was **not** declared active for this change in `config.yaml` /
parent prompt / (absent) `apply-progress.md`, so the formal TDD-cycle-evidence
check is **not applicable**. Assertion quality was still audited adversarially.
**Backend assertions — GENUINELY BEHAVIORAL (good).** Spot-checked:
- `test_prometheus_range.py`: asserts exact label strings (`instance=h:9100 mode=idle`), dedup suffix (`job=x (1)`), null sentinels → `None`, malformed-timestamp drop, point-count band per preset, 15s floor. No tautologies.
- `test_widgets.py` adapters: assert the **called URL** (`endswith("/api/v1/query_range")` / `/api/v1/query"`), the **params** (`{start,end,step} ⊆ params`, `query=="up"`), the **return shape** (`result["series"][0]["label"]`, `result["value"]==0.75`), multi-series → `{"error" ... "single-series"}`, and mean arithmetic (`2.0`, NaN-skip `3.0`). These verify real behavior, not smoke.
**Frontend assertions — adequate, one coverage gap.**
- Gauge test asserts the **rendered value text** (`0.75 %`, `42 req/s`) and threshold-label presence/absence — meaningful.
- Mean test asserts the **rendered value** (`23.5 %`, `1500`) — meaningful.
- Chart rendered-case asserts only the **SectionCard title** (`CPU Usage`), not that recharts drew the series SVG — a weak/title-only assertion (acceptable as a "rendered data case" since the component mounts with series data, but it does not prove the lines rendered).
**[WARNING] SC-125 loading-state gap (see §3).** None of the three widget tests
sets `isLoading:true`. The `mockData` helper hard-codes `isLoading:false`, and no
case asserts the `<Skeleton>` loading branch. Each component's loading branch
exists and is structurally identical to sibling widgets, so this is a
**test-coverage gap, not a functional defect**. Recommend adding one
`isLoading:true` case per widget to fully satisfy SC-125's enumerated
"loading state" requirement. Non-blocking.
No ghost loops, no type-only assertions, no implementation-detail CSS assertions
found. No test mocks grafana anywhere (only the whitelisted `Dashboard.test.tsx`
shortcut label).
## 6. Review-workload / PR-boundary findings
Per-slice changed lines (numstat, excluding `tasks.md` doc churn):
| Commit | Slice | Source Δ | Over 400? | Verdict |
|--------|-------|----------|-----------|---------|
| `5dad982` | 1 range+rebrand+helper | ~333 ins / ~56 del | under (337 net w/ test) | **OK** |
| `65bae95` | 2 gauge+mean | ~707 ins / ~32 del | **over** (largely new widgets+tests: gauge 137, mean 59, tests 124, backend tests 197) | **OK** — additive feature code+tests; forecast rated "Medium"; boundary is the feature, not a missed split |
| `67ca0fc` | 3 grafana removal+config+changelog | ~123 ins / **~926 del** | under (net negative) | **OK** — mostly deletions |
The `tasks.md` Review Workload Forecast (`stacked-to-main`, 3 slices, ~8501,110
total) was followed: S1→S2→S3, each independently green. Slice 2's +707 insertions
exceed the 400-line *added* budget but are dominated by two new components + their
tests + new adapter tests (no `size:exception` flag was recorded, and the forecast
itself rated slice 2 "~310400 changed lines" which under-counts the test volume).
This is a **minor forecast-vs-actual variance on an additive slice**, not scope
creep — the boundary is exactly the gauge+mean feature, no unrelated files touched.
Recommend recording the slice-2 actual in the archive summary. **Non-blocking.**
Scope was honored: no backend API/type-contract widening, no `service-storage-harness`
coupling, `metric` path preserved (`PrometheusMetricWidget` reads `data?.data?.result`
as the PromQL data object; `_instant_query` returns `{"result": payload.get("data",{})}`
— identical shape).
## 7. Adversarial checks
- **Dead imports after grafana removal?** None. Backend `ruff` (catches unused
imports) is clean; `sources.py` imports `WINDOW_PRESETS/normalize_prometheus_matrix/
step_for_window` — all used. Frontend `tsc`/`eslint` clean (unresolved imports
would fail the build).
- **`PrometheusWidgetSource.fetch` dispatch cross-contamination?** None. Clean
`widget_kind` dispatch: `chart``{series}`, `gauge``{value,...}`, `mean``{value}`,
default→`{result}` (metric). Distinct shapes; shared `_range_query`/`_instant_query`
only do HTTP + error mapping.
- **Config-schema consistency (backend models vs frontend registry)?** Consistent.
gauge: backend `{promql, warn_at, crit_at, min, max, unit}` ↔ frontend configSchema
`{promql(req), warn_at, crit_at, min, max, unit}`. chart: `{promql, window}`
`{promql(req), window}`. mean: `{promql, window, unit}``{promql(req), window, unit}`.
- **Lingering grafana mocks?** None in `.py`/`.tsx` source. Only stale `.pyc`
bytecode cache + `.pi-map.md`.
## 8. Residual risks / non-blocking findings
1. **[CRITICAL-process] 19 unchecked tasks + missing `apply-progress.md`** (§4) — archive blocker; reconciliation is doc-only.
2. **[WARNING] SC-125 loading-state tests missing** (§5) — coverage gap, not a defect.
3. **[INFO] Stale generated `.pi-map.md`** files reference Grafana / `ObservabilityPage.tsx` / `TestGrafanaStatus` / `GrafanaLinkWidget`. Not deliverable source; ignored by SC-115/116. Regenerate via `project_map_patch`/`project_map_validate` (the project-map protocol flags these `dirty`).
4. **[INFO] `config.yaml` context** still names `frontend/src/components/ObservabilityPage.tsx` (refactored away into `service-tabs/`). Not an SC-121 criterion (which targets the charting/grafana claims, which are fixed); minor doc staleness.
5. **[INFO] Uncommitted `MediaTab.tsx`** cosmetic reformat (Prettier line-wrap of a ternary) — unrelated to this change, predates/orthogonal. Dirty working tree; no files are staged.
6. **[INFO] Chunk-size build warning** (~1.1 MB JS) — non-fatal, pre-existing, orthogonal.
7. **No browser/visual smoke** performed (out of scope); the recharts `RadialBarChart` gauge and `LineChart` rendering are only structurally tested.
## 9. Exact blockers
- **BLOCKER (archive only, doc-reconcilable):** 19 unchecked implementation/verification
tasks (§3.13.14, §4.14.5) and absent `apply-progress.md`. Implementation is
verified complete; resolution = tick boxes + write `apply-progress.md`.
No code-level blockers. All functional requirements SC-101…SC-127 pass (SC-125
PARTIAL on test coverage only). All four gates green. **Code is ready for
`sdd-sync`; archive requires the checkbox/apply-progress reconciliation.**
## 10. Recommended next phase
**`sdd-sync`** (code PASS). Concurrently/after: author `apply-progress.md`
documenting the three landed slices, and tick §3.13.14 + §4.14.5 in
`tasks.md` to clear the archive blocker. Optionally add three `isLoading:true`
widget tests to move SC-125 PARTIAL→PASS, and regenerate the stale `.pi-map.md`.
---
### Appendix A — Verification commands run (at `67ca0fc`)
```
cd backend && PYTHONPATH=src python3 -m pytest -q → 293 passed (2 warnings)
cd backend && PYTHONPATH=src python3 -m ruff check src tests → All checks passed!
cd frontend && npm run build → exit 0 (✓ built; >500kB warning pre-existing)
cd frontend && npm run lint → exit 0 (0 errors, 1 pre-existing warning)
grep -rin grafana backend/src --include='*.py' → 3 docstring hits in prometheus_range.py (allowed)
grep -rin grafana frontend/src → Dashboard.test.tsx fixture + stale .pi-map.md only
grep -rin grafana backend/tests --include='*.py' → ZERO
ls backend/src/media_library_viewer_api/integrations/grafana.py → ENOENT (deleted)
grep -nE '^\s*- \[ \]' tasks.md → 19 unchecked (§3.13.14, §4.14.5)
```
### Appendix B — Files substantively changed
**Backend**
- `widgets/prometheus_range.py` (new) — `WINDOW_PRESETS`, `step_for_window`, `normalize_prometheus_matrix`.
- `widgets/sources.py` — dropped `GrafanaWidgetSource`; `PrometheusWidgetSource` gains `_fetch_chart/_fetch_gauge/_fetch_mean` + shared `_range_query/_instant_query`.
- `integrations/prometheus.py``metric`+`chart`+`gauge`+`mean` widget kinds + config models.
- `integrations/registry.py` — grafana import/entry removed.
- `integrations/grafana.py`**deleted**.
- `routers/monitoring.py``get_grafana_status` removed.
- `tests/test_prometheus_range.py` (new), `tests/test_widgets.py` (chart/gauge/mean adapter tests; grafana tests removed), `tests/test_api.py` + `tests/test_services.py` (grafana fixtures/tests removed).
**Frontend**
- `widgets/PrometheusChartWidget.tsx` (renamed from GrafanaChartWidget), `PrometheusGaugeWidget.tsx` (new), `PrometheusMeanWidget.tsx` (new) + their tests.
- `widgets/GrafanaLinkWidget.tsx`**deleted**; `widgets/index.ts` barrel updated.
- `integrations/registry.ts` — grafana binding removed; chart/gauge/mean added to prometheus; `registry.test.ts` updated.
- `integrations/navEntries.ts` — grafana entry removed.
- `hooks/useObservability.ts`, `api/client.ts`, `types/index.ts` — grafana hook/client/type removed.
- `pages/service-tabs/LinksTab.tsx` (+test) — **deleted**; `service-tabs/index.ts` grafana case removed.
- `pages/Dashboard.tsx` (`OBSERVABILITY_TYPES`), `pages/ServicesPage.tsx` (copy) — grafana removed.
**Docs**
- `openspec/config.yaml` — stale thin-dashboard/no-recharts claims repealed.
- `CHANGELOG.md` — direct-Prometheus-charting + grafana-removal migration note.