From 3e770751718d37464370257c6ec8b4d39f09234e Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 8 Jul 2026 22:57:29 +0000 Subject: [PATCH] spec(prometheus-direct-charting): sync into canonical prometheus-charting domain New canonical domain openspec/specs/prometheus-charting/spec.md with all 27 requirements (SC-101..127) as the durable post-change contract. Change-side delta specs/prometheus-charting/spec.md + sync-report.md. web-ui canonical untouched (different concern). --- .../specs/prometheus-charting/spec.md | 135 ++++++++++++++ .../prometheus-direct-charting/sync-report.md | 168 ++++++++++++++++++ openspec/specs/prometheus-charting/spec.md | 128 +++++++++++++ 3 files changed, 431 insertions(+) create mode 100644 openspec/changes/prometheus-direct-charting/specs/prometheus-charting/spec.md create mode 100644 openspec/changes/prometheus-direct-charting/sync-report.md create mode 100644 openspec/specs/prometheus-charting/spec.md diff --git a/openspec/changes/prometheus-direct-charting/specs/prometheus-charting/spec.md b/openspec/changes/prometheus-direct-charting/specs/prometheus-charting/spec.md new file mode 100644 index 0000000..d9c7723 --- /dev/null +++ b/openspec/changes/prometheus-direct-charting/specs/prometheus-charting/spec.md @@ -0,0 +1,135 @@ +# Prometheus Charting — Delta (`prometheus-direct-charting`) + +> Change: `prometheus-direct-charting` · Domain: `prometheus-charting` · Phase: **spec** (reconciled during `sdd-sync`). +> Distilled verbatim from the verified flat `spec.md` (27 requirements, SC-101 … SC-127) of change +> `prometheus-direct-charting`, cross-referenced against `design.md` and `verify-report.md`. Captures +> the **durable, post-change end-state contracts** for direct Prometheus-backed metric visualization +> and the Grafana removal that established the new model. + +## ADDED Requirements + +> The canonical `openspec/specs/prometheus-charting/spec.md` did not exist before this change. All +> requirements below are therefore **ADDED** to a new `prometheus-charting` domain; `sdd-sync` copies +> them into the canonical spec (native helper rule: when the canonical spec does not exist, the +> change spec becomes the new canonical spec). +> +> Requirement IDs (SC-101 … SC-127) and body text are preserved **exactly** from the verified flat +> `spec.md`, including the patched SC-116 / SC-118 wording. Requirements are grouped logically and +> listed in the following group order: +> +> - **Direct Prometheus range query path** — SC-101 … SC-104 +> - **Prometheus chart widget (rebrand + rebind)** — SC-105 … SC-108 +> - **Prometheus gauge widget** — SC-109 … SC-111 +> - **Prometheus mean widget** — SC-112 … SC-114 +> - **Grafana removal** — SC-115 … SC-120 +> - **Configuration documentation accuracy** — SC-121 … SC-122 +> - **Test and build greenness** — SC-123 … SC-125 +> - **Migration guidance** — SC-126 … SC-127 + +### Requirement: SC-101 — Prometheus range query returns the existing series shape + +When a `prometheus` widget of kind `chart` is fetched, the backend MUST query `{base_url}/api/v1/query_range` with `query`, `start`, `end`, and `step` derived from the widget config, and return a payload of shape `{ "series": [{ "label": str, "points": [{ "t": int, "v": float|null }] }] }` — the exact shape the frontend chart renderer already consumes. + +### Requirement: SC-102 — Series label normalization is shared and Prometheus-native + +The metric-label → readable-label normalization MUST live in a single shared helper (not duplicated in a Grafana path) and MUST produce meaningful labels for Prometheus matrix results, including deduplicating repeated labels via a `label (n)` suffix. + +### Requirement: SC-103 — Range query errors degrade gracefully + +A Prometheus timeout, connection error, or non-2xx response MUST cause the widget data fetch to return `{ "error": str }` (not raise), so the frontend renders the standard per-widget error state and the rest of the dashboard remains functional. + +### Requirement: SC-104 — Step is derived from the window preset + +Given a window preset (1h / 6h / 24h / 7d), the backend MUST derive a `step` that yields a reasonable number of points (target ~100–300 points). Users do not configure `step` directly. + +### Requirement: SC-105 — Chart widget moves from grafana to prometheus + +A widget kind named `chart` MUST be bound to the `prometheus` service type in both the backend registry and the frontend `SERVICE_REGISTRY`. The `grafana` service type MUST NOT offer a `chart` kind. + +### Requirement: SC-106 — Chart renderer is reused unchanged + +The recharts rendering (line chart, multi-series, axes, tooltip, `mergeSeries`, color tokens) MUST be preserved in the rebranded `PrometheusChartWidget`. The rename is structural; the rendering code is not rewritten. + +### Requirement: SC-107 — Chart supports multiple series + +The `chart` widget MUST render all series returned by the range query, each as its own line with a distinct color. There is no single-series restriction on `chart`. + +### Requirement: SC-108 — Chart window is a preset + +The `chart` widget config MUST expose the time window as a preset selector (`1h`, `6h`, `24h`, `7d`), not raw `from`/`to`/`step` fields. The preset is stored in widget config and resolved to `start`/`end` server-side. + +### Requirement: SC-109 — Gauge renders an instant scalar + +A widget kind named `gauge` MUST be bound to the `prometheus` service. Its data fetch MUST run an instant PromQL query (`/api/v1/query`) and return the scalar result for rendering as a gauge. + +### Requirement: SC-110 — Gauge supports configurable threshold bands + +The `gauge` widget config MUST accept optional threshold values (e.g. `warn_at`, `crit_at`) and the renderer MUST display green / amber / red bands accordingly. When thresholds are omitted, the gauge renders with a single neutral color and no bands. + +### Requirement: SC-111 — Gauge is scalar-only + +The `gauge` widget MUST render exactly one scalar value. If the instant query returns multiple series, the adapter MUST return `{ "error": str }` (not silently pick one), directing the user to refine the PromQL. + +### Requirement: SC-112 — Mean computes client-side over a window + +A widget kind named `mean` MUST be bound to the `prometheus` service. Its data fetch MUST run a range query over the configured window preset and return the arithmetic mean of all non-null point values as a single scalar. + +### Requirement: SC-113 — Mean uses plain PromQL + window preset + +The `mean` widget config MUST accept a plain PromQL expression (no requirement to wrap in `avg_over_time`) plus a window preset. Users do not write range-vector functions. + +### Requirement: SC-114 — Mean is scalar-only + +The `mean` widget MUST render exactly one scalar value. If the range query returns multiple series, the adapter MUST return `{ "error": str }` (not silently aggregate across series). + +### Requirement: SC-115 — No grafana references in backend source + +After the change, `grep -ri grafana backend/src --include='*.py'` MUST return no matches (excluding comments/changelog that are explicitly about the removal, if any are retained — but ideally zero). + +### Requirement: SC-116 — No grafana references in frontend source + +After the change, `grep -ri grafana frontend/src` MUST return no matches, **excluding** (a) test fixtures where "Grafana" appears as a user-authored dashboard *shortcut label* unrelated to the grafana service type (e.g. `Dashboard.test.tsx`), and (b) `LinksTab.tsx` / `service-tabs/index.ts` lines that are themselves being deleted as part of SC-118. (Source finding: `ObservabilityPage.tsx` was refactored into `service-tabs/`.) + +### Requirement: SC-117 — Grafana service type is gone from registries + +Neither the backend `SERVICE_DEFINITIONS` / `SERVICE_ADAPTERS` nor the frontend `SERVICE_REGISTRY` / `BUILTIN_WIDGETS` MUST contain a `grafana` entry. The `integrations/grafana.py` file MUST be deleted. + +### Requirement: SC-118 — Grafana status checks and UI sections are removed + +The `get_grafana_status` endpoint and its frontend hook (`useGrafanaStatus`) MUST be removed. The UI surface previously in `ObservabilityPage.tsx` has been refactored into a per-service-type `service-tabs/` architecture; the Grafana removal targets are therefore `service-tabs/LinksTab.tsx` + its test, the `grafana` case in `service-tabs/index.ts`, the grafana entry in `integrations/navEntries.ts`, the `grafana` member of `Dashboard.tsx`'s `OBSERVABILITY_TYPES` set, and any Grafana empty-state copy in `ServicesPage.tsx`. The literal `ObservabilityPage.tsx` no longer exists; SC-118's *intent* (no Grafana UI surface) is what is verified. + +### Requirement: SC-119 — Grafana widget instances degrade gracefully + +An existing persisted widget row referencing a `grafana` service MUST NOT crash the dashboard. It resolves to the existing "unknown widget" error state and surfaces a clear message; the operator can then delete it. + +### Requirement: SC-120 — Grafana tests are removed + +All Grafana-specific tests (backend and frontend) MUST be deleted; no test references grafana. + +### Requirement: SC-121 — config.yaml matches implementation + +`openspec/config.yaml` MUST NOT contain the stale claims "Do NOT re-implement charting in-app" or "No recharts/d3 is in use." It MUST reflect that in-app charting via `recharts` is the sanctioned approach for Prometheus-backed series, and MUST NOT reference Grafana as a chart path. + +### Requirement: SC-122 — CHANGELOG documents the migration + +`CHANGELOG.md` MUST include an entry instructing operators to delete existing Grafana service instances and recreate them as Prometheus services, noting that configured `grafana/chart` widgets must be recreated as `prometheus/chart` widgets. + +### Requirement: SC-123 — Backend tests pass + +`pytest` run from `backend/` MUST pass, including new tests covering: Prom range query → `{series}` normalization, gauge scalar-only enforcement, mean client-side aggregation, and the shared label helper. + +### Requirement: SC-124 — Frontend typechecks, builds, and lints + +`npm run build` (which runs `tsc -b` + `vite build`) and `npm run lint` from `frontend/` MUST pass. + +### Requirement: SC-125 — New widget kinds have tests + +`PrometheusChartWidget`, `PrometheusGaugeWidget`, and `PrometheusMeanWidget` MUST each have a frontend test covering at least: loading state, error state, and a rendered data case. + +### Requirement: SC-126 — No silent data migration + +The change MUST NOT attempt to auto-migrate existing `grafana` service rows into `prometheus` rows (URLs differ; true migration is impossible). Migration is operator-driven per the CHANGELOG note. + +### Requirement: SC-127 — Non-blocking on the service-storage-harness change + +This change MUST NOT depend on the `service-storage-harness` change. It is independently buildable, testable, and deployable. (The reverse dependency holds: the qBit speed widget depends on this change's chart capability.) diff --git a/openspec/changes/prometheus-direct-charting/sync-report.md b/openspec/changes/prometheus-direct-charting/sync-report.md new file mode 100644 index 0000000..124dcc0 --- /dev/null +++ b/openspec/changes/prometheus-direct-charting/sync-report.md @@ -0,0 +1,168 @@ +# Sync Report — `prometheus-direct-charting` + +> Phase: **sync** · Change: `prometheus-direct-charting` · Repo: `/home/user/manage` +> Mode: file-backed (`artifactStore: openspec`). No source-code edits; only OpenSpec artifacts were +> written. Not committed (parent owns the commit). The change folder was **not** moved (that is +> `sdd-archive`'s job). + +**Status: SYNCED.** A new canonical domain `openspec/specs/prometheus-charting/spec.md` was created +from the verified change, and the change-side domain delta spec that unblocks the native status +engine's `sync`/`archive` gates is also in place. + +--- + +## 1. Executive summary + +The `prometheus-direct-charting` change shipped a **complete but flat** `openspec/changes/prometheus-direct-charting/spec.md` +(27 requirements, SC-101 … SC-127) with **no** per-domain delta spec under +`openspec/changes/prometheus-direct-charting/specs//`. `sdd-sync` requires a domain delta +spec; the flat spec alone does not satisfy the canonical-merge contract. + +Verify already returned **PASS** (verdict in `verify-report.md`; all four gates green — backend +`pytest` 293 passed, `ruff` clean, frontend `npm run build` exit 0, `npm run lint` 0 errors). +Functional coverage was 26/27 fully PASS, with SC-125 PARTIAL on test coverage only (loading-state +test gap) — a non-blocking coverage finding, not a functional defect; the task framing treats it as +PARTIAL→PASS after coverage close. The verify report's single CRITICAL was an **archive** blocker +(19 unchecked task checkboxes + missing `apply-progress.md`); the `apply-progress.md` artifact now +exists and that condition does **not** block `sdd-sync` of the green code. + +This sync **reconciles** the flat-spec-vs-domain-spec gap: + +1. Authored the missing **change-side domain delta spec** — + `openspec/changes/prometheus-direct-charting/specs/prometheus-charting/spec.md` — using a clean + `## ADDED Requirements` structure that preserves the exact requirement IDs (SC-101 … SC-127) and + text (including the patched SC-116 / SC-118 wording) from the verified flat `spec.md`. This is + what flips the native status engine's `specs` artifact from partial → done. +2. **Synced** the end-state into the **canonical store** — + `openspec/specs/prometheus-charting/spec.md` — the actual sync target. Because the canonical + `prometheus-charting` domain did not previously exist, the native helper rule applies: *when the + canonical spec does not exist, the change spec becomes the new canonical spec.* The two files + therefore carry identical requirement bodies (delta under `## ADDED Requirements`; canonical + under `## Requirements`), verified byte-identical for the requirement region. + +Domain name **`prometheus-charting`** was chosen (per the dispatch brief) because it covers the +full new model: direct Prometheus range-query charting, the gauge and mean modes, and the Grafana +removal that established the new direct-query model. It is distinct from the existing canonical +`web-ui` domain (MUI→shadcn migration — a different concern), which was **not touched**. + +## 2. Structured status & actionContext findings + +The native `gentle-pi.sdd-status` passed by the parent reports `changeName: null` with +`blockedReasons: ["Change selection is ambiguous: mobile-responsive-parity, prometheus-direct-charting, +prometheus-direct-charting, service-storage-harness, services-as-hub-ia."]` because the engine +auto-detected four active changes. This sync task was **explicitly assigned** +`prometheus-direct-charting`; the ambiguity is a parent-resolution artifact and does not block this +phase (`isNonAuthoritative: false`). + +- `artifactStore: openspec`; change root `openspec/changes/prometheus-direct-charting/`. +- Artifacts present: `proposal.md`, `spec.md`, `design.md`, `tasks.md`, `verify-report.md`, + `apply-progress.md`. +- `verify: PASS` (verify-report verdict; gates green at `67ca0fc`). +- `actionContext`: `mode: repo-local`, `workspaceRoot: /home/user/manage`, + `allowedEditRoots: ["/home/user/manage"]`, `warnings: []`. All three files written are inside the + authoritative workspace / allowed edit roots. ✓ +- `relationships.sameDomainActiveChanges: []`, `collisions: []` — **no active same-domain + collisions**, so no archive/sync ordering decision was required. +- The new `prometheus-charting` domain is distinct from the existing `web-ui` canonical domain; + `openspec/specs/web-ui/spec.md` was left untouched. + +**Post-sync structural change:** `openspec/changes/prometheus-direct-charting/specs/prometheus-charting/spec.md` +now exists (`hasDomainSpecs` → true), resolving the missing-domain-spec condition that gated sync. +The flat `spec.md` is intentionally **left in place** as the authoritative planning artifact the +work was built against (the archive convention keeps flat specs too); it no longer triggers the +"flat spec without domain specs" condition now that a domain delta sits alongside it. + +## 3. Domains synced & canonical files updated + +| Domain | Change-side delta (source) | Canonical (sync target) | Action | +|---|---|---|---| +| `prometheus-charting` | `openspec/changes/prometheus-direct-charting/specs/prometheus-charting/spec.md` | `openspec/specs/prometheus-charting/spec.md` | **NEW domain** — `## ADDED Requirements` copied into canonical as a new spec | + +- **Canonical file created:** `openspec/specs/prometheus-charting/spec.md` (27 requirements). +- **Change-side delta created:** `openspec/changes/prometheus-direct-charting/specs/prometheus-charting/spec.md` + (27 requirements, all `## ADDED Requirements`). + +## 4. Requirement delta (ADDED / MODIFIED / REMOVED) + +- **ADDED (27)** — all to the new `prometheus-charting` domain (canonical did not exist pre-change). + IDs and text preserved verbatim from the verified flat `spec.md`. Grouped logically: + - *Direct Prometheus range query path* — SC-101, SC-102, SC-103, SC-104 + - *Prometheus chart widget (rebrand + rebind)* — SC-105, SC-106, SC-107, SC-108 + - *Prometheus gauge widget* — SC-109, SC-110, SC-111 + - *Prometheus mean widget* — SC-112, SC-113, SC-114 + - *Grafana removal* — SC-115, SC-116, SC-117, SC-118, SC-119, SC-120 + - *Configuration documentation accuracy* — SC-121, SC-122 + - *Test and build greenness* — SC-123, SC-124, SC-125 + - *Migration guidance* — SC-126, SC-127 +- **MODIFIED (0)** — none (new domain; no pre-existing canonical requirements to replace). +- **REMOVED (0)** — none. +- **RENAMED (0)** — none (RENAMED is intentionally unsupported by the native delta helper; not used). + +## 5. Guardrails, approvals & destructive-sync assessment + +- **Same-domain collisions:** none (`sameDomainActiveChanges: []`, `collisions: []`). The new + `prometheus-charting` domain does not overlap the existing `web-ui` canonical domain. No ordering + decision was needed. +- **Destructive sync:** **not applicable.** There are zero REMOVED requirements and zero large + MODIFIED blocks (new domain; everything is ADDED). No destructive-sync parent approval was + required for this sync beyond the explicit reconciliation instruction in the task. +- **Legacy flat spec:** detected pre-sync; resolved by adding the domain delta spec alongside it + (the block condition is specifically "flat spec *without* domain specs"). The flat spec was left + in place as a planning artifact. +- **`web-ui` canonical isolation:** the existing `openspec/specs/web-ui/spec.md` (the MUI→shadcn + rework) was **not modified** — verified untouched by `git status` (mtime `2026-06-17T19:11`, + not in the modified set). The two domains are independent. + +## 6. Validation / checks performed (file-backed, read-only) + +Run from `/home/user/manage` (no source edits, no test re-runs — those are owned by verify and were +already green at `67ca0fc`): + +| Check | Command | Result | +|---|---|---| +| Canonical store populated | `ls openspec/specs/prometheus-charting/spec.md` | present ✓ | +| Change-side domain spec present | `ls openspec/changes/prometheus-direct-charting/specs/prometheus-charting/spec.md` | present ✓ | +| Requirement-ID parity (flat ↔ delta ↔ canonical) | `grep -oE 'SC-[0-9]+'` all three files, `sort -u` | **27 == 27 == 27**, identical IDs SC-101…SC-127 ✓ | +| Body-text parity (delta ↔ canonical) | `diff` of the `^### Requirement:` region of both files | **identical** ✓ | +| Patched SC-116 wording present | `grep "service-tabs/"` both delta + canonical | present in both ✓ | +| Patched SC-118 wording present | `grep "service-tabs/LinksTab.tsx"` both delta + canonical | present in both ✓ | +| Delta is pure ADDED | count `## ADDED/MODIFIED/REMOVED/RENAMED Requirements` | ADDED=1, MODIFIED=0, REMOVED=0, RENAMED=0 ✓ (no destructive sync) | +| `web-ui` canonical untouched | `git status --porcelain openspec/specs/web-ui/spec.md` | empty (not modified) ✓ | +| No edits outside openspec | `git status --porcelain` (filtered) | only `openspec/specs/prometheus-charting/`, `openspec/changes/prometheus-direct-charting/specs/`, and this report added; pre-existing dirty/untracked items unrelated to this sync unchanged ✓ | +| Markdown validity | write-time lint | all three files "Markdown clean" ✓ | + +## 7. Carry-over items for the archive summary + +These verify-phase findings are non-blocking for sync and should land in the archive summary: + +1. **[INFO] SC-125 was PARTIAL in verify** (no explicit `isLoading:true` loading-state test in any of + the three new widget test files — coverage gap, not a functional defect). The task framing treats + this as PARTIAL→PASS after coverage close; if a loading-state case per widget has not been added, + `sdd-archive` may want to confirm or note it. +2. **[CRITICAL-process, archive-only] Unchecked task checkboxes.** At verify time, 19 implementation + / verification task checkboxes (Slice 3 §3.1–3.14 and Integration §4.1–4.5) were unchecked and + `apply-progress.md` was missing. `apply-progress.md` now exists (created after the verify pass); + `sdd-archive` should re-scan the native status engine to confirm `tasks: done` / `applyProgress: + present` before moving the change to archive, and tick any remaining unchecked boxes if needed. +3. **[INFO] Stale generated `.pi-map.md`** files still reference Grafana / `ObservabilityPage.tsx` + (generated artifacts, not deliverable source; ignored by SC-115/116). Regenerate via + `project_map_patch` / `project_map_validate`. +4. **[INFO] Slice-2 review-budget variance** (~707 insertions vs ~310–400 forecast) — additive + feature code + tests; boundary is the gauge+mean feature, not scope creep. Non-blocking; record + in the archive summary. + +## 8. Next recommended phase + +→ **`sdd-archive`** (clean). Confirm the native status re-scan reports `specs: done` / `sync: ready` +/ `archive: ready`, then move the change to +`openspec/changes/archive/YYYY-MM-DD-prometheus-direct-charting`, carrying over the items in §7 into +the archive summary. Do **not** commit or push — the parent owns the commit with explicit paths. + +--- + +### Appendix — Files written by this sync (OpenSpec only; no source code) + +- `openspec/changes/prometheus-direct-charting/specs/prometheus-charting/spec.md` — **change-side + domain delta (`## ADDED Requirements`), 27 requirements SC-101…SC-127.** +- `openspec/specs/prometheus-charting/spec.md` — **canonical spec (new domain), 27 requirements.** +- `openspec/changes/prometheus-direct-charting/sync-report.md` — this report. diff --git a/openspec/specs/prometheus-charting/spec.md b/openspec/specs/prometheus-charting/spec.md new file mode 100644 index 0000000..44e9c90 --- /dev/null +++ b/openspec/specs/prometheus-charting/spec.md @@ -0,0 +1,128 @@ +# Prometheus Charting + +> Domain: `prometheus-charting` · **Canonical specification.** Synced from change `prometheus-direct-charting`. +> +> This is the merged end-state of cutting Grafana out of the metric-visualization path and making +> Prometheus the direct data source. It captures the durable, post-change contracts for in-app +> Prometheus-backed metric rendering, not the per-slice delivery strategy (which remains on record +> in the change's `spec.md` / `tasks.md` under `openspec/changes/prometheus-direct-charting/`). + +## Purpose + +Define WHAT must be true of Manage's metric-visualization surface after the change: Prometheus is +queried **directly** (no Grafana middleman) to back a multi-series line **chart**, a scalar +**gauge**, and a windowed **mean**, alongside the existing instant-query numeric **metric** widget. +In-app charting via `recharts` is the sanctioned renderer; the `grafana` service type, its widgets, +status endpoint, and UI surfaces are fully removed; orphaned Grafana widget instances degrade +gracefully; and operators migrate by recreating services/widgets (no silent data migration). This +spec is acceptance-focused and verifiable; it deliberately does not prescribe implementation. + +## Requirements + +### Requirement: SC-101 — Prometheus range query returns the existing series shape + +When a `prometheus` widget of kind `chart` is fetched, the backend MUST query `{base_url}/api/v1/query_range` with `query`, `start`, `end`, and `step` derived from the widget config, and return a payload of shape `{ "series": [{ "label": str, "points": [{ "t": int, "v": float|null }] }] }` — the exact shape the frontend chart renderer already consumes. + +### Requirement: SC-102 — Series label normalization is shared and Prometheus-native + +The metric-label → readable-label normalization MUST live in a single shared helper (not duplicated in a Grafana path) and MUST produce meaningful labels for Prometheus matrix results, including deduplicating repeated labels via a `label (n)` suffix. + +### Requirement: SC-103 — Range query errors degrade gracefully + +A Prometheus timeout, connection error, or non-2xx response MUST cause the widget data fetch to return `{ "error": str }` (not raise), so the frontend renders the standard per-widget error state and the rest of the dashboard remains functional. + +### Requirement: SC-104 — Step is derived from the window preset + +Given a window preset (1h / 6h / 24h / 7d), the backend MUST derive a `step` that yields a reasonable number of points (target ~100–300 points). Users do not configure `step` directly. + +### Requirement: SC-105 — Chart widget moves from grafana to prometheus + +A widget kind named `chart` MUST be bound to the `prometheus` service type in both the backend registry and the frontend `SERVICE_REGISTRY`. The `grafana` service type MUST NOT offer a `chart` kind. + +### Requirement: SC-106 — Chart renderer is reused unchanged + +The recharts rendering (line chart, multi-series, axes, tooltip, `mergeSeries`, color tokens) MUST be preserved in the rebranded `PrometheusChartWidget`. The rename is structural; the rendering code is not rewritten. + +### Requirement: SC-107 — Chart supports multiple series + +The `chart` widget MUST render all series returned by the range query, each as its own line with a distinct color. There is no single-series restriction on `chart`. + +### Requirement: SC-108 — Chart window is a preset + +The `chart` widget config MUST expose the time window as a preset selector (`1h`, `6h`, `24h`, `7d`), not raw `from`/`to`/`step` fields. The preset is stored in widget config and resolved to `start`/`end` server-side. + +### Requirement: SC-109 — Gauge renders an instant scalar + +A widget kind named `gauge` MUST be bound to the `prometheus` service. Its data fetch MUST run an instant PromQL query (`/api/v1/query`) and return the scalar result for rendering as a gauge. + +### Requirement: SC-110 — Gauge supports configurable threshold bands + +The `gauge` widget config MUST accept optional threshold values (e.g. `warn_at`, `crit_at`) and the renderer MUST display green / amber / red bands accordingly. When thresholds are omitted, the gauge renders with a single neutral color and no bands. + +### Requirement: SC-111 — Gauge is scalar-only + +The `gauge` widget MUST render exactly one scalar value. If the instant query returns multiple series, the adapter MUST return `{ "error": str }` (not silently pick one), directing the user to refine the PromQL. + +### Requirement: SC-112 — Mean computes client-side over a window + +A widget kind named `mean` MUST be bound to the `prometheus` service. Its data fetch MUST run a range query over the configured window preset and return the arithmetic mean of all non-null point values as a single scalar. + +### Requirement: SC-113 — Mean uses plain PromQL + window preset + +The `mean` widget config MUST accept a plain PromQL expression (no requirement to wrap in `avg_over_time`) plus a window preset. Users do not write range-vector functions. + +### Requirement: SC-114 — Mean is scalar-only + +The `mean` widget MUST render exactly one scalar value. If the range query returns multiple series, the adapter MUST return `{ "error": str }` (not silently aggregate across series). + +### Requirement: SC-115 — No grafana references in backend source + +After the change, `grep -ri grafana backend/src --include='*.py'` MUST return no matches (excluding comments/changelog that are explicitly about the removal, if any are retained — but ideally zero). + +### Requirement: SC-116 — No grafana references in frontend source + +After the change, `grep -ri grafana frontend/src` MUST return no matches, **excluding** (a) test fixtures where "Grafana" appears as a user-authored dashboard *shortcut label* unrelated to the grafana service type (e.g. `Dashboard.test.tsx`), and (b) `LinksTab.tsx` / `service-tabs/index.ts` lines that are themselves being deleted as part of SC-118. (Source finding: `ObservabilityPage.tsx` was refactored into `service-tabs/`.) + +### Requirement: SC-117 — Grafana service type is gone from registries + +Neither the backend `SERVICE_DEFINITIONS` / `SERVICE_ADAPTERS` nor the frontend `SERVICE_REGISTRY` / `BUILTIN_WIDGETS` MUST contain a `grafana` entry. The `integrations/grafana.py` file MUST be deleted. + +### Requirement: SC-118 — Grafana status checks and UI sections are removed + +The `get_grafana_status` endpoint and its frontend hook (`useGrafanaStatus`) MUST be removed. The UI surface previously in `ObservabilityPage.tsx` has been refactored into a per-service-type `service-tabs/` architecture; the Grafana removal targets are therefore `service-tabs/LinksTab.tsx` + its test, the `grafana` case in `service-tabs/index.ts`, the grafana entry in `integrations/navEntries.ts`, the `grafana` member of `Dashboard.tsx`'s `OBSERVABILITY_TYPES` set, and any Grafana empty-state copy in `ServicesPage.tsx`. The literal `ObservabilityPage.tsx` no longer exists; SC-118's *intent* (no Grafana UI surface) is what is verified. + +### Requirement: SC-119 — Grafana widget instances degrade gracefully + +An existing persisted widget row referencing a `grafana` service MUST NOT crash the dashboard. It resolves to the existing "unknown widget" error state and surfaces a clear message; the operator can then delete it. + +### Requirement: SC-120 — Grafana tests are removed + +All Grafana-specific tests (backend and frontend) MUST be deleted; no test references grafana. + +### Requirement: SC-121 — config.yaml matches implementation + +`openspec/config.yaml` MUST NOT contain the stale claims "Do NOT re-implement charting in-app" or "No recharts/d3 is in use." It MUST reflect that in-app charting via `recharts` is the sanctioned approach for Prometheus-backed series, and MUST NOT reference Grafana as a chart path. + +### Requirement: SC-122 — CHANGELOG documents the migration + +`CHANGELOG.md` MUST include an entry instructing operators to delete existing Grafana service instances and recreate them as Prometheus services, noting that configured `grafana/chart` widgets must be recreated as `prometheus/chart` widgets. + +### Requirement: SC-123 — Backend tests pass + +`pytest` run from `backend/` MUST pass, including new tests covering: Prom range query → `{series}` normalization, gauge scalar-only enforcement, mean client-side aggregation, and the shared label helper. + +### Requirement: SC-124 — Frontend typechecks, builds, and lints + +`npm run build` (which runs `tsc -b` + `vite build`) and `npm run lint` from `frontend/` MUST pass. + +### Requirement: SC-125 — New widget kinds have tests + +`PrometheusChartWidget`, `PrometheusGaugeWidget`, and `PrometheusMeanWidget` MUST each have a frontend test covering at least: loading state, error state, and a rendered data case. + +### Requirement: SC-126 — No silent data migration + +The change MUST NOT attempt to auto-migrate existing `grafana` service rows into `prometheus` rows (URLs differ; true migration is impossible). Migration is operator-driven per the CHANGELOG note. + +### Requirement: SC-127 — Non-blocking on the service-storage-harness change + +This change MUST NOT depend on the `service-storage-harness` change. It is independently buildable, testable, and deployable. (The reverse dependency holds: the qBit speed widget depends on this change's chart capability.)