Files
manage/openspec/changes/service-registry/tasks.md
T
Developer 9459de5c07 docs(service-registry): lock decisions (cascade delete, required key, SSH runner model)
- §11 decisions: cascade-delete services with widgets; MANAGE_ENCRYPTION_KEY
  always required; SSH task runner is multi-instance with reusable tasks.
- §12 SSH task runner model: instances absorb SSH task transport, tasks stay
  global/reusable with default_service_id, service_task_runs logs history,
  widget config { task_id, service_id? }.
- tasks.md: add service_task_runs table + cascade-delete tests to Slice 1,
  SSH run-logging to Slice 2, follow-ups (Actions rebuild, machine unification).
2026-06-22 11:14:01 +00:00

213 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tasks: Runtime Service Registry
**Change:** `service-registry`
**Phase:** tasks
**Date:** 2026-06-19
## Review workload forecast
| Field | Value |
|-------|-------|
| Estimated changed lines | ~2,0002,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`.