Files
manage/openspec/changes/prometheus-direct-charting/verify-report.md
T
Developer 7440603cdb 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.
2026-07-08 22:50:30 +00:00

337 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.