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.
9.6 KiB
9.6 KiB
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 |
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 missingMANAGE_ENCRYPTION_KEY. Addcryptographydependency if missing.
- Files:
- 1.2 Add integrations base classes
- Files:
integrations/__init__.py,integrations/base.py(new) - Lines: ~80
- Details:
ServiceDefinition,WidgetKind,SecretField,ServiceConfigBase.
- Files:
- 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
ServiceDefinitionper service with config schema, secret fields, and widget kinds.SERVICE_DEFINITIONS+get_service_definition/get_widget_kindhelpers.
- Files:
- 1.4 Add service store +
servicestable- Files:
services/settings_store.py(modify),services/service_store.py(new) - Lines: ~120
- Details:
servicestable ininit_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 theservice_task_runstable (design §12.3) now so later slices can populate it.
- Files:
- 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.
- Files:
- 1.6 Validate encryption key on startup
- Files:
auth.pyormain.pylifespan (modify) - Lines: ~10
- Details: Extend startup validation to require
MANAGE_ENCRYPTION_KEY.
- Files:
- 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.
- Files:
- 1.8 Verify
- Run:
cd backend && .venv/bin/ruff check . && PYTHONPATH=src .venv/bin/python -m pytest
- Run:
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_kindtodashboard_widgets; keepwidget_typeas{service_type}.{kind}during transition; dropaddon_id.
- Files:
- 2.2 Refactor source adapters
- Files:
widgets/sources.py(modify) - Lines: ~160
- Details: Each adapter takes
(service: ServiceRecord, widget_kind, config).SOURCE_ADAPTERSkeyed byservice_type. Jellyfin/Grafana/Prometheus/SSH adapters resolve connection from the service record. The SSH adapter resolves the task + instance, runs it, and appends aservice_task_runsrow (design §12.3).
- Files:
- 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.
- Files:
- 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.
- Files:
- 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.
- Files:
- 2.6 Verify
- Run:
cd backend && .venv/bin/ruff check . && PYTHONPATH=src .venv/bin/python -m pytest
- Run:
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 gainsservice_id,widget_kind.
- Files:
- 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.
- Files:
- 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.
- Files:
- 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).
- Files:
- 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.
- Files:
- 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.
- Files:
- 3.7 Verify
- Run:
cd frontend && npm run lint && npm run build && npm run test -- src/integrations/registry.test.ts
- Run:
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.
- Files:
- 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 fromservice_task_runs.
- Files:
- 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.
- Files:
- 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_KEYto compose.env.example.
- Files:
- 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.
- Files:
- 4.6 Docs + changelog
- Files:
docs/REQUIREMENTS.md,README.md,docs/CHANGELOG.md(new or modify) - Lines: ~80
- Details: Service registry section;
MANAGE_ENCRYPTION_KEYrequirement; breaking upgrade note (re-enter Jellyfin config).
- Files:
- 4.7 Verify full stack
- Run: backend
ruff+pytest; frontendlint+build+test.
- Run: backend
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
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.