# Apply Progress: Grafana Metric Gateway

**Change:** `grafana-metric-gateway`
**Phase:** apply-progress
**Date:** 2026-07-09
**Status:** complete — all 33 tasks done, all gates green, verified (see `verify-report.md`)

## Slices delivered

Two slices, each its own commit, each leaving `pytest` / `npm run build` / `npm run lint` / `ruff` green.

### Slice 1 — Backend gateway transport (commit `df80c68`, amended)

- `integrations/prometheus.py` — `PrometheusConfig` rewritten: dropped `base_url`; added `grafana_url: ServiceBaseUrl` + `datasource_uid: str = "prometheus"` + kept `timeout_seconds`; secret_fields now `grafana_api_key` (required, encrypted). Widget kinds chart/gauge/mean/metric unchanged (GM-101).
- `widgets/prometheus_range.py` — added `normalize_grafana_frames(raw)` refactored from the recovered `65bae95` code; shares `_dedup_label` with the retained `normalize_prometheus_matrix` (DRY). `step_for_window` preserved (qBit dependency) (GM-104).
- `widgets/sources.py` — renamed `PrometheusWidgetSource` → `MetricSource` (SERVICE_ADAPTERS key stays `"prometheus"`). All four kinds (`chart`/`gauge`/`mean`/`metric`) now route through one `_gateway_query` POSTing `{grafana_url}/api/ds/query` with `Authorization: Bearer {api_key}` + datasource `{uid, type:"prometheus"}`; window presets map to `from`/`intervalMs`/`maxDataPoints`. **Zero direct `/api/v1/query[_range]` calls remain** (GM-102/106/107/108/109).
- `routers/monitoring.py` — `get_prometheus_status` runs `expr:"up"` through the gateway; maps failures (auth/unreachable/datasource) (GM-110).
- `main.py` — startup old-config validation: detects persisted prometheus services with the old shape (`base_url`/no `grafana_url`) and logs a migration warning. Best-effort, no crash (GM-113).
- `CHANGELOG.md` — `[Unreleased]` BREAKING entry: reconfigure prometheus services with `grafana_url`/`datasource_uid`/`grafana_api_key` (GM-114).
- Tests rewritten: chart/gauge/mean adapter tests assert `POST /api/ds/query` + frames mock (not direct Prom). `test_prometheus_range.py` gains `normalize_grafana_frames` tests.

### Slice 2 — Frontend widget renames (commit `7e91e7f`)

- `git mv` (R100, history preserved): `PrometheusChartWidget.tsx` → `MetricChartWidget.tsx` (+ test); same for Gauge + Mean. Exports renamed; tests updated (GM-111).
- `integrations/registry.ts` — `prometheus` binding's component refs updated to `MetricChartWidget`/`MetricGaugeWidget`/`MetricMeanWidget`. Kinds stay `chart`/`gauge`/`mean`/`metric`. Barrel `widgets/index.ts` updated (GM-112).
- `PrometheusMetricWidget` deliberately KEPT under that name (design §3.1) — it's the instant-query numeric widget, adapted (design §3.4 Option A) to read the gateway's normalized `{result:[{label,points}]}` shape via last-point extraction.
- Rendering unchanged: `LineSeriesChart`, gauge bands, mean windowing preserved verbatim. qBit widgets untouched.

## Deviations from tasks.md

- None functional. `PrometheusMetricWidget` was not renamed (design §3.1 explicitly kept it); its data-shape was adapted per design §3.4 Option A.

## Final gate results

| Gate | Result |
|---|---|
| `backend && PYTHONPATH=src python3 -m pytest -q` | **331 passed**, 2 warnings (pre-existing pythonjsonlogger DeprecationWarning) |
| `backend && PYTHONPATH=src python3 -m ruff check src tests` | **All checks passed** |
| `frontend && npm run build` | **exit 0** (pre-existing chunk-size warning) |
| `frontend && npm run lint` | **0 errors**, 1 pre-existing warning (`WidgetConfigDialog.tsx`, untouched) |
| `frontend && npx vitest run` | **151 passed** — renamed Metric* tests + qBit speed (LineSeriesChart non-regression) all green |

## Verification

See `verify-report.md` — adversarial fresh-context review: **15/16 PASS, 1 PARTIAL** (GM-115 coverage gap: no startup-validation or status-auth-failed enumerated tests; code paths correct). No blocking code findings. The partial is a coverage gap, not a defect; can be closed with two small tests if desired before archive.
