docs(openspec): add SDD artifacts for next changes

- jellyfin-service-registry: proposal, design, and tasks for completing
  the Jellyfin migration off machine-level config.
- grafana-prometheus-polish: proposal, design, and tasks for improving
  the Grafana/Prometheus observability integration.

Both are planning-only artifacts; implementation not started.
This commit is contained in:
Developer
2026-06-23 20:40:35 +00:00
parent ca8927834e
commit 4d520ab0e3
6 changed files with 391 additions and 0 deletions
@@ -0,0 +1,64 @@
# Design — Grafana/Prometheus integration polish
**Change:** `grafana-prometheus-polish`
**Phase:** design
**Date:** 2026-06-23
## Current state
- Backend integrations: `integrations/grafana.py` (config `base_url`, secret `api_key`, widget `link`) and `integrations/prometheus.py` (config `base_url`, secret `api_key`, widget `metric`).
- Backend widget sources: `widgets/sources.py` has `GrafanaWidgetSource` and `PrometheusWidgetSource`.
- Frontend `SERVICE_REGISTRY` (`frontend/src/integrations/registry.ts`) mirrors those widget kinds.
- Observability page (`frontend/src/components/ObservabilityPage.tsx`) uses hard-coded `VITE_GRAFANA_URL` for Grafana links and shows only Alertmanager + Prometheus targets.
- Monitoring router (`backend/src/media_library_viewer_api/routers/monitoring.py`) exposes `/alerts`, `/alertmanager-status`, `/alertmanager-webhook`, `/prometheus-targets`, `/machines`.
## Target state
### Backend
1. **New health endpoints**
- `GET /api/monitoring/grafana-status` — probes `GET {base_url}/api/health` (no auth needed for health) and returns `{up, version, error?}`.
- `GET /api/monitoring/prometheus-status` — probes `GET {base_url}/-/healthy` and returns `{up, version, error?}`.
- If multiple instances of a type exist, probe the first enabled instance and include its `service_id` + `name` in the response.
- If no instance is configured, return `up: false, error: "no_service_configured"`.
2. **New widget kinds**
- Grafana: add `explore_link` kind with config `query` (optional), `datasource` (optional). Renders a deep-link to `/explore?orgId=1&left={...}` or similar.
- Prometheus: add `range_chart` kind with config `promql`, `step` (e.g. `1m`), `range_seconds` (e.g. `3600`). Renders `{ series: [{ timestamp, value }] }` for the frontend to chart. The adapter performs a range query (`/api/v1/query_range`) and reshapes the result.
3. **Source adapters**
- Extend `GrafanaWidgetSource.fetch` to handle `explore_link`.
- Extend `PrometheusWidgetSource.fetch` to handle `range_chart`.
### Frontend
1. **Observability page**
- Replace hard-coded `GRAFANA_BASE_URL` with discovery from `useServiceInstances("grafana")`.
- Add `useGrafanaStatus()` and `usePrometheusStatus()` hooks using the new endpoints.
- Add health cards for Grafana and Prometheus next to Alertmanager.
- Grafana links (alerts panel, targets panel) use the discovered instance URL.
2. **Widget registry**
- Add `explore_link` to `SERVICE_REGISTRY.grafana.widgets`.
- Add `range_chart` to `SERVICE_REGISTRY.prometheus.widgets`.
- Add presentational components `GrafanaExploreLinkWidget` and `PrometheusRangeChartWidget` (or extend existing components).
3. **Types**
- Add status response types and widget config types.
### Docs
- Update `docs/REQUIREMENTS.md` observability section to describe service-instance-driven Grafana/Prometheus status and new widget kinds.
- `CHANGELOG.md` added/changed note.
## Slices
1. **Backend health endpoints + widget kind extensions.**
2. **Frontend Observability page + widget registry + new components.**
3. **Docs + changelog.**
## Risks
- Grafana Explore URL encoding is version-dependent. Target Grafana 10+ and document the assumption.
- Prometheus range queries can be expensive; cap `range_seconds` in the adapter (e.g. max 24h) and use a modest `step`.
- Multiple Grafana/Prometheus instances: keep it simple — use the first enabled instance for status; widgets already bind to a specific service instance.
@@ -0,0 +1,44 @@
# Proposal — Grafana/Prometheus integration polish
**Change:** `grafana-prometheus-polish`
**Phase:** proposal
**Date:** 2026-06-23
## Problem
Grafana and Prometheus are already modeled as service-registry service types and provide dashboard widgets (`GrafanaLinkWidget`, `PrometheusMetricWidget`). The Observability page already shows Alertmanager alerts, Prometheus targets, and a hard-coded Grafana base URL (`VITE_GRAFANA_URL`).
However, the integration is still piecemeal:
1. **Hard-coded Grafana URL** in `frontend/src/components/ObservabilityPage.tsx` (`GRAFANA_BASE_URL = import.meta.env.VITE_GRAFANA_URL || "http://localhost:3000"`). This ignores any Grafana service instances configured in the registry.
2. **No Grafana health/status card** on the Observability page. Users cannot see whether their configured Grafana instance is reachable.
3. **No Prometheus health/status card**. The targets endpoint shows scrape config, but not whether Prometheus itself is up.
4. **Prometheus widget only supports instant queries**. There is no range-query chart widget or table widget.
5. **Grafana widget only supports dashboard deep-links**. There is no "open Explore with a query" shortcut.
6. **Observability page mixes service-bound and hard-coded sources**. It should derive Grafana/Prometheus URLs from configured service instances instead of env vars.
## Goal
Polish the Grafana/Prometheus integration so the Observability page and dashboard widgets feel first-class:
1. Observability page discovers configured Grafana and Prometheus service instances and uses their `base_url` instead of `VITE_GRAFANA_URL`.
2. Add health/status cards for Grafana and Prometheus (reachable/unreachable + version if available).
3. Add backend health-check endpoints for Grafana (`/api/monitoring/grafana-status`) and Prometheus (`/api/monitoring/prometheus-status`) that proxy a lightweight health endpoint.
4. Extend widget kinds:
- Prometheus: add `range_chart` widget kind for simple sparkline charts.
- Grafana: add `explore_link` widget kind to open Grafana Explore with a pre-filled query.
5. Keep the existing `link` and `metric` widgets backward-compatible.
## Non-goals
- Do NOT build a full in-app metric explorer or charting library.
- Do NOT store Prometheus query history or persist widget state beyond existing dashboard widgets.
- Do NOT replace the Alertmanager proxy or Prometheus targets endpoint.
## Acceptance
- `VITE_GRAFANA_URL` is no longer required; the Observability page falls back to configured Grafana instances.
- `/api/monitoring/grafana-status` and `/api/monitoring/prometheus-status` return `{up, version, error?}`.
- New widget kinds are registered in backend integrations and frontend `SERVICE_REGISTRY`.
- Frontend lint/build/test and backend pytest remain green.
- Docs/CHANGELOG updated.
@@ -0,0 +1,106 @@
# Tasks — Grafana/Prometheus integration polish
**Change:** `grafana-prometheus-polish`
**Phase:** tasks
**Date:** 2026-06-23
## Review workload forecast
| Field | Value |
|-------|-------|
| Estimated changed lines | ~400600 |
| Chained PRs recommended | Yes (2 PRs) |
| Chain strategy | stacked-to-main |
## Slice 1: Backend — health endpoints + widget extensions
**Goal:** Surface Grafana/Prometheus health and extend widget kinds.
- [ ] **1.1 Add Grafana status endpoint**
- Files: `backend/src/media_library_viewer_api/routers/monitoring.py`
- Lines: ~35
- Details: `GET /api/monitoring/grafana-status` discovers the first enabled `grafana` service, probes `/api/health`, returns `{ up, version, service_id, name, error? }`.
- [ ] **1.2 Add Prometheus status endpoint**
- Files: `backend/src/media_library_viewer_api/routers/monitoring.py`
- Lines: ~35
- Details: `GET /api/monitoring/prometheus-status` discovers the first enabled `prometheus` service, probes `/-/healthy` and `/api/v1/status/buildinfo` for version, returns `{ up, version, service_id, name, error? }`.
- [ ] **1.3 Add `explore_link` widget kind to Grafana integration**
- Files: `backend/src/media_library_viewer_api/integrations/grafana.py`, `backend/src/media_library_viewer_api/widgets/sources.py`
- Lines: ~30
- Details: New `GrafanaExploreWidgetConfig` with `query` and `datasource`; `GrafanaWidgetSource.fetch` builds an `/explore` deep-link.
- [ ] **1.4 Add `range_chart` widget kind to Prometheus integration**
- Files: `backend/src/media_library_viewer_api/integrations/prometheus.py`, `backend/src/media_library_viewer_api/widgets/sources.py`
- Lines: ~60
- Details: New `PrometheusRangeChartWidgetConfig` with `promql`, `range_seconds`, `step`; adapter calls `/api/v1/query_range`, caps `range_seconds` at 86400, reshapes to `{ series: [{ timestamp, value }] }`.
- [ ] **1.5 Update backend tests**
- Files: `backend/tests/test_api.py`, `backend/tests/test_widgets.py`
- Lines: ~40
- Details: Test status endpoints with mocked HTTP; test new widget kinds.
- [ ] **1.6 Verify backend**
- Run: `cd backend && .venv/bin/ruff check . && PYTHONPATH=src .venv/bin/python -m pytest`
**Slice 1 total:** ~200 changed lines.
## Slice 2: Frontend — Observability page + widget registry
**Goal:** Remove hard-coded Grafana URL and surface new widgets.
- [ ] **2.1 Add status hooks**
- Files: `frontend/src/hooks/useObservability.ts`, `frontend/src/api/client.ts`
- Lines: ~30
- Details: `fetchGrafanaStatus`, `fetchPrometheusStatus`, `useGrafanaStatus()`, `usePrometheusStatus()`.
- [ ] **2.2 Update Observability page**
- Files: `frontend/src/components/ObservabilityPage.tsx`
- Lines: ~80
- Details: Discover Grafana/Prometheus services; replace `GRAFANA_BASE_URL`; add health cards; use discovered URLs for links.
- [ ] **2.3 Extend widget registry**
- Files: `frontend/src/integrations/registry.ts`, `frontend/src/widgets/*`
- Lines: ~80
- Details: Add `GrafanaExploreLinkWidget` and `PrometheusRangeChartWidget` (or extend existing); register new kinds; update `registry.test.ts`.
- [ ] **2.4 Update types**
- Files: `frontend/src/types/index.ts`
- Lines: ~20
- Details: Add `GrafanaStatus`, `PrometheusStatus`, and widget config types.
- [ ] **2.5 Update frontend tests**
- Files: `frontend/src/components/__tests__/ObservabilityPage.test.tsx` if it exists, or relevant widget tests
- Lines: ~30
- Details: Mock new hooks/services; assert health cards render.
- [ ] **2.6 Verify frontend**
- Run: `cd frontend && npm run lint && npm run build && npm run test`
**Slice 2 total:** ~240 changed lines.
## Slice 3: Docs + changelog
**Goal:** Document the polished integration.
- [ ] **3.1 Update REQUIREMENTS.md**
- Files: `docs/REQUIREMENTS.md`
- Lines: ~20
- Details: Observability section describes service-instance-driven Grafana/Prometheus status cards and new widget kinds.
- [ ] **3.2 Update CHANGELOG.md**
- Files: `CHANGELOG.md`
- Lines: ~15
- Details: Added status endpoints, removed hard-coded `VITE_GRAFANA_URL` dependency, new widget kinds.
- [ ] **3.3 Commit**
- Commit message: `feat(observability): polish Grafana/Prometheus integration`
## Acceptance
- `/api/monitoring/grafana-status` and `/api/monitoring/prometheus-status` return sensible JSON for configured/missing/unreachable services.
- Observability page no longer references `VITE_GRAFANA_URL`.
- New widget kinds appear in the "Add widget" flow and render data.
- Backend and frontend test suites pass.
- Docs and changelog updated.
@@ -0,0 +1,50 @@
# Design — Jellyfin service-registry migration (completion)
**Change:** `jellyfin-service-registry`
**Phase:** design
**Date:** 2026-06-23
## Current state
- Jellyfin/Jellyseerr service definitions exist in `integrations/jellyfin.py` and `integrations/jellyseerr.py`.
- `dependencies.py` resolves Jellyfin clients from service records via `jellyfin_service_id`.
- Frontend pages already select Jellyfin services via `useServiceInstances("jellyfin")`.
- `settings_store.py` still defaults machine `services` to `["monitoring", "files", "jellyfin"]` and persists `media_root` / `path_prefix`.
- `MonitoringMachine` / `MonitoringMachineInput` types still include `media_root` and `path_prefix`.
- `Settings.tsx` still renders `media_root` and `path_prefix` inputs and lists `jellyfin` as a service option.
- `path_utils.py` still has path-mapping helpers, but the callers in `files.py` no longer depend on machine-level `media_root`/`path_prefix`.
## Target state
### Backend
- `DEFAULT_SERVICES` becomes `["monitoring", "files"]`.
- `settings_store.py` drops `media_root` and `path_prefix` from `_normalize_machine_payload`, `_row_to_machine`, the machine table schema, and default seeding.
- Add `DROP TABLE IF EXISTS ...` or `ALTER TABLE DROP COLUMN` cleanup as needed for existing databases (SQLite has limited `DROP COLUMN` support; prefer table rebuild or leave harmless columns).
- `path_utils.py` is reviewed; if no callers remain, delete the module. If callers remain, ensure they do not reference machine fields.
- `routers/files.py` confirmed to not read machine `media_root`/`path_prefix`.
- Tests updated to remove `media_root`/`path_prefix` from fixtures.
### Frontend
- Remove `media_root` and `path_prefix` from `MonitoringMachine` and `MonitoringMachineInput` in `types/index.ts`.
- Remove `jellyfin` from the machine services multi-select in `Settings.tsx`.
- Remove `media_root` and `path_prefix` inputs from the machine editor.
- Update `emptyMachine()` to not include these fields.
- Update tests/fixtures.
### Docs
- `docs/REQUIREMENTS.md` — update the machine config description to remove Jellyfin/path-prefix fields.
- `CHANGELOG.md` — breaking note about removed machine fields.
## Slices
1. **Backend cleanup**`settings_store.py`, `config.py` (if any env vars), `path_utils.py`, tests.
2. **Frontend cleanup** — types, `Settings.tsx`, tests.
3. **Docs + changelog**.
## Risks
- Existing DB rows may have `media_root`/`path_prefix` values; dropping columns is safe but irreversible. Backups recommended.
- `path_utils.py` may still be referenced by archive code; verify only live code is affected.
@@ -0,0 +1,41 @@
# Proposal — Jellyfin service-registry migration (completion)
**Change:** `jellyfin-service-registry`
**Phase:** proposal
**Date:** 2026-06-23
## Problem
The service-registry foundation already moved Jellyfin/Jellyseerr resolution into `dependencies.py` (`get_jellyfin_client`, `get_jellyseerr_client`, `get_user_id`) using `jellyfin_service_id`. The frontend `Applications`, `Media`, and `Users` pages already consume `useServiceInstances("jellyfin")`.
However, the **machine-level config still carries legacy Jellyfin DNA** that is now dead or misleading:
- `DEFAULT_SERVICES = ["monitoring", "files", "jellyfin"]` in `settings_store.py` auto-tags every local machine as a Jellyfin source.
- The Settings machine editor still exposes `services` multi-select that can include `jellyfin`, even though Jellyfin is selected via the Services page, not via a machine.
- `MonitoringMachine` types still carry `media_root` and `path_prefix` fields whose original purpose was Jellyfin→SSH path mapping, but the path-mapping logic in `path_utils.py` now operates independently and those fields are no longer read by the media/file flows.
- Documentation still describes machine-level Jellyfin fields in places.
This creates UI confusion (two places to think about Jellyfin) and stale surface area.
## Goal
Cleanly complete the Jellyfin migration onto the service registry:
1. Remove Jellyfin from the machine `services` taxonomy entirely.
2. Remove dead `media_root` / `path_prefix` machine fields from backend, frontend types, Settings form, and tests.
3. Update path-resolution helpers to rely only on the Jellyfin service config (or explicit path-prefix settings on the service record if needed), not on machine fields.
4. Update docs and changelog.
## Non-goals
- Do NOT change how Jellyfin/Jellyseerr clients are resolved in `dependencies.py` — that is already correct.
- Do NOT remove the `jellyfin` / `jellyseerr` service definitions.
- Do NOT add new service types or widgets in this change.
## Acceptance
- `grep -R "media_root\|path_prefix" backend/src frontend/src` returns only `path_utils.py` and its tests (if those helpers remain) or nothing (if the helpers are removed).
- `jellyfin` no longer appears in machine `services` options in the Settings UI.
- `DEFAULT_SERVICES` no longer includes `jellyfin`.
- Backend and frontend test suites remain green.
- Docs/CHANGELOG explain the final migration.
@@ -0,0 +1,86 @@
# Tasks — Jellyfin service-registry migration (completion)
**Change:** `jellyfin-service-registry`
**Phase:** tasks
**Date:** 2026-06-23
## Review workload forecast
| Field | Value |
|-------|-------|
| Estimated changed lines | ~250350 |
| Chained PRs recommended | No (single PR) |
| Chain strategy | n/a |
## Slice 1: Backend cleanup
**Goal:** Remove Jellyfin and dead path-mapping fields from machine config.
- [ ] **1.1 Drop `jellyfin` from default machine services**
- Files: `backend/src/media_library_viewer_api/services/settings_store.py`
- Lines: ~5
- Details: Change `DEFAULT_SERVICES = ["monitoring", "files", "jellyfin"]` to `["monitoring", "files"]`. Update all fallback/default service lists.
- [ ] **1.2 Remove `media_root` and `path_prefix` machine fields**
- Files: `backend/src/media_library_viewer_api/services/settings_store.py`
- Lines: ~40
- Details: Remove from machine table schema (if present), `_normalize_machine_payload`, `_row_to_machine`, default seeding, and any serialization helpers.
- [ ] **1.3 Review/delete `path_utils.py`**
- Files: `backend/src/media_library_viewer_api/path_utils.py`, `backend/tests/test_path_utils.py`
- Lines: ~-120 if deleted, else ~10 to confirm callers
- Details: Check if any live router still imports the helpers. If not, delete module and tests.
- [ ] **1.4 Update backend tests**
- Files: `backend/tests/test_api.py`, `backend/tests/test_widgets.py`, others as needed
- Lines: ~20
- Details: Remove `media_root`/`path_prefix` from machine fixtures.
- [ ] **1.5 Verify backend**
- Run: `cd backend && .venv/bin/ruff check . && PYTHONPATH=src .venv/bin/python -m pytest`
## Slice 2: Frontend cleanup
**Goal:** Remove dead machine fields and the Jellyfin service option from Settings.
- [ ] **2.1 Update types**
- Files: `frontend/src/types/index.ts`
- Lines: ~10
- Details: Remove `media_root` and `path_prefix` from `MonitoringMachine` and `MonitoringMachineInput`.
- [ ] **2.2 Update Settings form**
- Files: `frontend/src/pages/Settings.tsx`
- Lines: ~60
- Details: Remove `media_root` and `path_prefix` inputs, remove `jellyfin` from the services multi-select options, update `emptyMachine()` and edit reset logic.
- [ ] **2.3 Update frontend tests**
- Files: `frontend/src/pages/__tests__/Settings.test.tsx` if it exists, or other fixtures
- Lines: ~15
- Details: Remove fields from mock machines.
- [ ] **2.4 Verify frontend**
- Run: `cd frontend && npm run lint && npm run build && npm run test`
## Slice 3: Docs + changelog
**Goal:** Document the completed migration and removed fields.
- [ ] **3.1 Update REQUIREMENTS.md**
- Files: `docs/REQUIREMENTS.md`
- Lines: ~10
- Details: Remove references to machine-level `media_root`/`path_prefix` and Jellyfin service tagging.
- [ ] **3.2 Update CHANGELOG.md**
- Files: `CHANGELOG.md`
- Lines: ~10
- Details: Add a breaking/changed note: machine config no longer includes `media_root`, `path_prefix`, or `jellyfin` in `services`; Jellyfin is configured exclusively via Services.
- [ ] **3.3 Commit**
- Commit message: `refactor(settings): complete Jellyfin migration onto service registry`
## Acceptance
- `grep -R "media_root\|path_prefix" backend/src frontend/src` returns only `path_utils.py` and tests if retained, or nothing if deleted.
- `jellyfin` is not offered as a machine service in the Settings UI.
- Backend `pytest` and frontend `npm run lint/build/test` all pass.
- Docs and changelog updated.