Files
manage/openspec/changes/service-registry/apply-progress.md
T
Developer 8cdeadd6dd feat(services): backend service registry foundation (encryption, definitions, CRUD)
PR 1 of 4 for the runtime service registry change.

- Add Fernet encryption helper (services/secrets.py) with a required
  MANAGE_ENCRYPTION_KEY; validate it on startup.
- Add closed integrations/ registry with Pydantic config + widget-config
  definitions for grafana, prometheus, jellyfin, nextcloud, and ssh_tasks.
- Add services + service_task_runs tables and SettingsStore CRUD with
  cascade-delete (defensive until widgets carry service_id).
- Add /api/services/types and /api/services/instances CRUD (encrypted secrets,
  secrets_set flags only; never plaintext).
- Declare cryptography as a direct dependency.
- Require MANAGE_ENCRYPTION_KEY in compose + .env.example + README.
- Add 25 backend tests (registry, encryption, CRUD, cascade, task-run history).

Verification: ruff clean; pytest 225 passed; frontend lint/build green.
2026-06-22 12:56:03 +00:00

3.7 KiB

Apply Progress: Runtime Service Registry

Change: service-registry Apply run: PR 1 / Slice 1 — Backend service foundation Date: 2026-06-19

Completed tasks (Slice 1)

  • 1.1 Add encryption helper (services/secrets.py)
  • 1.2 Add integrations base classes (integrations/base.py)
  • 1.3 Add five service definitions + registry
  • 1.4 Add services + service_task_runs tables + store CRUD with cascade delete
  • 1.5 Add service Pydantic models + /api/services* router
  • 1.6 Validate MANAGE_ENCRYPTION_KEY on startup
  • 1.7 Add backend tests (tests/test_services.py)
  • 1.8 Verify (ruff + pytest green)

Files changed (Slice 1)

New files

  • backend/src/media_library_viewer_api/integrations/__init__.py — package marker.
  • backend/src/media_library_viewer_api/integrations/base.pyServiceConfigBase, WidgetConfigBase, SecretField, WidgetKind, ServiceDefinition, widget_kind(), validate_config().
  • backend/src/media_library_viewer_api/integrations/{grafana,prometheus,jellyfin,nextcloud,ssh_tasks}.py — one Pydantic-config + widget-config definition per service.
  • backend/src/media_library_viewer_api/integrations/registry.py — closed SERVICE_DEFINITIONS + helpers.
  • backend/src/media_library_viewer_api/services/secrets.py — Fernet encrypt/decrypt
    • key validation.
  • backend/src/media_library_viewer_api/models/services.py — request/response models.
  • backend/src/media_library_viewer_api/routers/services.py/api/services/types
    • /api/services/instances CRUD.
  • backend/tests/test_services.py — 25 tests.

Modified files

  • backend/src/media_library_viewer_api/services/settings_store.pyservices and service_task_runs tables; service CRUD; cascade delete (defensive against the not-yet-present dashboard_widgets.service_id column); task-run history helpers.
  • backend/src/media_library_viewer_api/main.py — register services_router; validate encryption key on startup.
  • backend/pyproject.toml — declare cryptography>=42.0 direct dependency.
  • docker-compose.yml, docker-compose.dev.yml, .env.example, README.md — require and document MANAGE_ENCRYPTION_KEY.

Verification (Slice 1)

cd backend
.venv/bin/ruff check .                                      # All checks passed
PYTHONPATH=src .venv/bin/python -m pytest                   # 225 passed
cd ../frontend
npm run lint                                                # 0 errors
npm run build                                               # success

Smoke: encryption round-trip OK; missing MANAGE_ENCRYPTION_KEY raises on startup.

Deviations from design

  • Service-config and widget-config schemas are derived from Pydantic models (model_json_schema()), matching the user's "proper pydantic config definitions" request. The design's hand-written JSON schemas were replaced by model-derived ones.
  • Service-table CRUD lives on SettingsStore (not a separate service_store.py) to match how widgets/saved_tasks/ssh_keys are already handled there. This keeps a single store owner for all tables.
  • The cascade delete defensively checks for dashboard_widgets.service_id (added in Slice 2) so Slice 1 stays green without the column.

Remaining work

  • Slice 2: Backend widget rebind to services (add service_id/widget_kind, refactor adapters to take a ServiceRecord, retire old widget registry, SSH run logging).
  • Slice 3: Frontend services runtime (types, API, hooks, frontend registry, service pages, route swap).
  • Slice 4: Dashboard picker, settings rework, remove grafana_url/prometheus_url env vars, stop default seeding, docs + changelog.