# Tasks: Runtime Service Registry **Change:** `service-registry` **Phase:** tasks **Date:** 2026-06-19 ## Review workload forecast | Field | Value | |-------|-------| | Estimated changed lines | ~2,000–2,400 | | 400-line budget risk | High | | Chained PRs recommended | Yes (4 PRs) | | Chain strategy | stacked-to-main | ```text Decision needed before apply: Yes (see design §11 open questions) Chained PRs recommended: Yes Chain strategy: stacked-to-main ``` ## Slice 1: Backend service foundation (no widget changes) **Goal:** Persist service instances with encrypted secrets and expose CRUD + metadata. - [ ] **1.1 Add encryption helper** - Files: `backend/src/media_library_viewer_api/services/secrets.py` (new) - Lines: ~60 - Details: Fernet-based `encrypt_secrets` / `decrypt_secrets` / `get_encryption_key`. Raise on missing `MANAGE_ENCRYPTION_KEY`. Add `cryptography` dependency if missing. - [ ] **1.2 Add integrations base classes** - Files: `integrations/__init__.py`, `integrations/base.py` (new) - Lines: ~80 - Details: `ServiceDefinition`, `WidgetKind`, `SecretField`, `ServiceConfigBase`. - [ ] **1.3 Add five service definitions + registry** - Files: `integrations/grafana.py`, `prometheus.py`, `jellyfin.py`, `nextcloud.py`, `ssh_tasks.py`, `integrations/registry.py` (new) - Lines: ~220 - Details: One `ServiceDefinition` per service with config schema, secret fields, and widget kinds. `SERVICE_DEFINITIONS` + `get_service_definition` / `get_widget_kind` helpers. - [ ] **1.4 Add service store + `services` table** - Files: `services/settings_store.py` (modify), `services/service_store.py` (new) - Lines: ~120 - Details: `services` table in `init_schema`; CRUD helpers; decrypt-on-read for adapters; "set" flags for the API without plaintext. **Cascade delete:** removing a service deletes its widgets in the same transaction. Also add the `service_task_runs` table (design §12.3) now so later slices can populate it. - [ ] **1.5 Add service Pydantic models + router** - Files: `models/services.py` (new), `routers/services.py` (new), `main.py` (modify) - Lines: ~110 - Details: `GET /api/services/types`, `GET /api/services`, `POST/PUT/DELETE /api/services/{id}`. Validate type, config, and secret schema against the definition. - [ ] **1.6 Validate encryption key on startup** - Files: `auth.py` or `main.py` lifespan (modify) - Lines: ~10 - Details: Extend startup validation to require `MANAGE_ENCRYPTION_KEY`. - [ ] **1.7 Add backend tests** - Files: `backend/tests/test_services.py` (new) - Lines: ~140 - Details: Registry contents, CRUD round-trip, secret encryption/decryption, unknown service type → 422, missing/invalid encryption key → startup error, cascade-delete removes a service's widgets. - [ ] **1.8 Verify** - Run: `cd backend && .venv/bin/ruff check . && PYTHONPATH=src .venv/bin/python -m pytest` **Slice 1 total:** ~720 changed lines (smallest coherent backend foundation). ## Slice 2: Backend widget rebind to services **Goal:** Widgets reference a service instance + widget kind; adapters resolve services. - [ ] **2.1 Add widget columns + migrate table** - Files: `services/settings_store.py` (modify) - Lines: ~40 - Details: Add `service_id`, `widget_kind` to `dashboard_widgets`; keep `widget_type` as `{service_type}.{kind}` during transition; drop `addon_id`. - [ ] **2.2 Refactor source adapters** - Files: `widgets/sources.py` (modify) - Lines: ~160 - Details: Each adapter takes `(service: ServiceRecord, widget_kind, config)`. `SOURCE_ADAPTERS` keyed by `service_type`. Jellyfin/Grafana/Prometheus/SSH adapters resolve connection from the service record. The SSH adapter resolves the task + instance, runs it, and **appends a `service_task_runs` row** (design §12.3). - [ ] **2.3 Retire old widget registry** - Files: `widgets/registry.py` (delete or hollow out), `widgets/__init__.py` - Lines: ~-60 - Details: Widget metadata now comes from `integrations/registry.py`. - [ ] **2.4 Update widgets router + models** - Files: `routers/widgets.py`, `models/widgets.py` (modify) - Lines: ~90 - Details: Validation uses the service definition's widget schema; data endpoint loads service, builds `ServiceRecord`, calls adapter. - [ ] **2.5 Update widget tests** - Files: `backend/tests/test_widgets.py` (modify) - Lines: ~120 - Details: Rewrite adapter/data tests around service instances; cover service-missing, wrong-kind, and encrypted-secret resolution. - [ ] **2.6 Verify** - Run: `cd backend && .venv/bin/ruff check . && PYTHONPATH=src .venv/bin/python -m pytest` **Slice 2 total:** ~330 changed lines. ## Slice 3: Frontend services runtime **Goal:** Service types/API/hooks, frontend service registry, service pages, route swap. - [ ] **3.1 Add service types** - Files: `frontend/src/types/index.ts` (modify) - Lines: ~50 - Details: `ServiceInstance`, `ServiceInstanceInput`, `ServiceTypeInfo`, `ServiceWidgetKind`. Widget gains `service_id`, `widget_kind`. - [ ] **3.2 Add services API + hooks** - Files: `frontend/src/api/services.ts`, `frontend/src/hooks/useServices.ts` (new) - Lines: ~110 - Details: Fetch/create/update/delete service instances and types. - [ ] **3.3 Add frontend service registry** - Files: `frontend/src/integrations/registry.ts` (new) - Lines: ~120 - Details: Closed registry mirroring backend: config fields, secret fields (`secret: true`), widget kinds, service page components. - [ ] **3.4 Add service page + components** - Files: `frontend/src/pages/ServicePage.tsx`, `frontend/src/integrations/components/*` (new) - Lines: ~180 - Details: Generic page dispatches by service type; renders config editor + widget kinds. Add per-service components (Grafana, Prometheus, Jellyfin, Nextcloud, SSH tasks). - [ ] **3.5 Swap routes; remove addon pages** - Files: `frontend/src/App.tsx`, `frontend/src/pages/AddonPage.tsx`, `frontend/src/addons/*` (modify/delete) - Lines: ~-40 net - Details: `/services/:serviceType/:serviceId`; redirect old `/addons/*` to the default service of that type. - [ ] **3.6 Add frontend registry test** - Files: `frontend/src/integrations/registry.test.ts` (new) - Lines: ~40 - Details: Assert all five service types and their widget kinds. - [ ] **3.7 Verify** - Run: `cd frontend && npm run lint && npm run build && npm run test -- src/integrations/registry.test.ts` **Slice 3 total:** ~460 changed lines. ## Slice 4: Dashboard picker, settings rework, cleanup, docs **Goal:** End-to-end service-based dashboard; remove legacy machine app config + env vars. - [ ] **4.1 Rework widget config dialog** - Files: `frontend/src/components/WidgetConfigDialog.tsx` (modify) - Lines: ~120 - Details: "Add widget" = pick service → pick widget kind → configure. Widget cards show parent service name. - [ ] **4.2 Update widget components to service model** - Files: `frontend/src/widgets/*` (modify) - Lines: ~120 - Details: Components read `widget_kind`; data shapes unchanged but sourced from the service adapter. SSH task widget shows last run status from `service_task_runs`. - [ ] **4.3 Remove machine Jellyfin/Jellyseerr fields** - Files: `frontend/src/pages/Settings.tsx`, `frontend/src/types/index.ts` (modify) - Lines: ~-60 - Details: Machines are SSH/monitoring transport only. - [ ] **4.4 Remove grafana_url / prometheus_url from backend config** - Files: `backend/src/media_library_viewer_api/config.py`, `docker-compose.yml`, `docker-compose.dev.yml`, `.env.example` - Lines: ~-10 - Details: URLs now live on service records. Add `MANAGE_ENCRYPTION_KEY` to compose - `.env.example`. - [ ] **4.5 Stop default widget seeding** - Files: `services/settings_store.py` (modify) - Lines: ~-20 - Details: Fresh installs start with no widgets; user adds them after configuring services. - [ ] **4.6 Docs + changelog** - Files: `docs/REQUIREMENTS.md`, `README.md`, `docs/CHANGELOG.md` (new or modify) - Lines: ~80 - Details: Service registry section; `MANAGE_ENCRYPTION_KEY` requirement; breaking upgrade note (re-enter Jellyfin config). - [ ] **4.7 Verify full stack** - Run: backend `ruff` + `pytest`; frontend `lint` + `build` + `test`. **Slice 4 total:** ~330 changed lines. ## Integration and acceptance - [ ] **5.1 Backend full test run** — `PYTHONPATH=src pytest`, all green. - [ ] **5.2 Frontend full build/lint/test** — `npm run lint && npm run build && npm run test`. - [ ] **5.3 Manual dev-stack check** — `docker compose -f docker-compose.dev.yml up --build`: - Create a Grafana service from the UI; verify the dashboard link widget works. - Create a Jellyfin service; verify the activity widget resolves it. - Delete a service with widgets → widgets are cascade-deleted and the service is gone. - Restart the stack; secrets remain usable (key stable). - Missing `MANAGE_ENCRYPTION_KEY` → backend refuses to start. - SSH task runner: define two instances, run the same reusable task against each, and see both runs in the instance's history log. ## Guards ```text Decision needed before apply: No (design §11 resolved) Chained PRs recommended: Yes Chain strategy: stacked-to-main 400-line budget risk: High ``` ## Explicit follow-ups (out of scope for this change) - Rebuild the Actions page UI on top of services (global reusable tasks + `default_service_id`), replacing the current machine-based saved-task runner. - Unify machines under services so an SSH host is defined once (today machines still own File Browser + node_exporter transport; see design §12.5). - Key rotation / re-encrypt workflow for `MANAGE_ENCRYPTION_KEY`.