ca8927834e
Move finished change directories to openspec/changes/archive/: - configurable-dashboard-widgets - decommission-monitoring-poller - service-registry - unify-tasks-on-services All associated implementation has been merged to main.
213 lines
9.6 KiB
Markdown
213 lines
9.6 KiB
Markdown
# 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`.
|