# 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.1–3.14 and Integration §4.1–4.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 (100–300 pts) | **PASS** | `step_for_window = max(15, round(window/200))`. Parametrized test asserts 100–300 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) => )` 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: (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.1–1.10): all 10 `[x]`** ✓ - **Slice 2 (§2.1–2.10): all 10 `[x]`** ✓ - **Slice 3 (§3.1–3.14): all 14 `[ ]` — UNCHECKED** - **Integration (§4.1–4.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.1–3.14 and §4.1–4.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 `` 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, ~850–1,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 "~310–400 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.1–3.14, §4.1–4.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.1–3.14 + §4.1–4.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.1–3.14, §4.1–4.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.