Files
Developer ca8927834e chore(openspec): archive completed changes
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.
2026-06-23 19:38:34 +00:00

9.6 KiB
Raw Permalink Blame History

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
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 runPYTHONPATH=src pytest, all green.
  • 5.2 Frontend full build/lint/testnpm run lint && npm run build && npm run test.
  • 5.3 Manual dev-stack checkdocker 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.