feat(widgets): rebind widgets to the service registry
PR 2 of 4 for the runtime service registry change. - dashboard_widgets gains service_id + widget_kind columns (legacy addon_id/widget_type kept but unused). - Source adapters take (service: ServiceRecord | None, widget_kind, config). SERVICE_ADAPTERS keyed by service_type; BUILTIN_ADAPTERS for backups/static. - Backups and static stay as service-less built-ins (service_id nullable), exposed via GET /api/widgets/builtin. - SSH task adapter resolves the task + instance, runs over SSH, and appends a service_task_runs history row on success/failure/timeout/error. - Retire widgets/registry.py; widget metadata now comes from the integrations registry + widgets/builtin. Remove /api/widgets/types and /api/widgets/sources. - Stop default widget seeding (fresh install = empty dashboard). - Rewrite widget tests around the service-bound + built-in model (26 tests). Backend-only breaking change; frontend is reconciled in Slice 3. Build/lint stay green; pytest 222 passed.
This commit is contained in:
@@ -1,79 +1,78 @@
|
||||
# Apply Progress: Runtime Service Registry
|
||||
|
||||
**Change:** `service-registry`
|
||||
**Apply run:** PR 1 / Slice 1 — Backend service foundation
|
||||
**Apply run:** PR 1 + PR 2 / Slice 1 + Slice 2
|
||||
**Date:** 2026-06-19
|
||||
|
||||
## Completed tasks (Slice 1)
|
||||
## Slice 1 — Backend service foundation (MERGED)
|
||||
|
||||
- [x] 1.1 Add encryption helper (`services/secrets.py`)
|
||||
- [x] 1.2 Add integrations base classes (`integrations/base.py`)
|
||||
- [x] 1.3 Add five service definitions + registry
|
||||
- [x] 1.4 Add `services` + `service_task_runs` tables + store CRUD with cascade delete
|
||||
- [x] 1.5 Add service Pydantic models + `/api/services*` router
|
||||
- [x] 1.6 Validate `MANAGE_ENCRYPTION_KEY` on startup
|
||||
- [x] 1.7 Add backend tests (`tests/test_services.py`)
|
||||
- [x] 1.8 Verify (ruff + pytest green)
|
||||
Completed in PR #7. See git history. Summary: Fernet secrets helper, closed
|
||||
`integrations/` registry with Pydantic config + widget-config definitions for
|
||||
grafana/prometheus/jellyfin/nextcloud/ssh_tasks, `services` + `service_task_runs`
|
||||
tables with cascade delete, `/api/services*` CRUD, `MANAGE_ENCRYPTION_KEY`
|
||||
required at startup, 25 tests.
|
||||
|
||||
## Files changed (Slice 1)
|
||||
## Slice 2 — Backend widget rebind to services (this PR)
|
||||
|
||||
### New files
|
||||
### Completed tasks
|
||||
|
||||
- `backend/src/media_library_viewer_api/integrations/__init__.py` — package marker.
|
||||
- `backend/src/media_library_viewer_api/integrations/base.py` — `ServiceConfigBase`,
|
||||
`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.
|
||||
- [x] 2.1 Add `service_id` / `widget_kind` columns to `dashboard_widgets`
|
||||
(additive ALTER; legacy `addon_id`/`widget_type` kept but unused).
|
||||
- [x] 2.2 Refactor source adapters to `fetch(service, widget_kind, config)`
|
||||
with `ServiceRecord | None`. `SERVICE_ADAPTERS` keyed by service_type;
|
||||
`BUILTIN_ADAPTERS` for backups/static. SSH adapter resolves the task +
|
||||
instance, runs, and appends a `service_task_runs` row (success/failure/
|
||||
timeout/error).
|
||||
- [x] 2.3 Retire old `widgets/registry.py` (deleted; metadata now comes from
|
||||
`integrations/registry` + `widgets/builtin`).
|
||||
- [x] 2.4 Update widgets router + models for service-bound + built-in widgets.
|
||||
Removed `/api/widgets/types` and `/api/widgets/sources`; added
|
||||
`/api/widgets/builtin`.
|
||||
- [x] 2.5 Rewrite widget tests around the new model.
|
||||
- [x] 2.6 Stop default widget seeding (fresh install = empty dashboard).
|
||||
|
||||
### Modified files
|
||||
### Decision resolved mid-slice
|
||||
|
||||
- `backend/src/media_library_viewer_api/services/settings_store.py` — `services` 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`.
|
||||
Backups and static widgets stay as **service-less built-ins** (`service_id`
|
||||
nullable), per product decision. The data endpoint resolves built-ins via
|
||||
`BUILTIN_ADAPTERS` and service-bound widgets via `SERVICE_ADAPTERS` + a
|
||||
decrypted `ServiceRecord`.
|
||||
|
||||
## Verification (Slice 1)
|
||||
### Files changed (Slice 2)
|
||||
|
||||
- New: `widgets/builtin.py` (built-in kinds + light config validation).
|
||||
- Rewritten: `widgets/sources.py` (`ServiceRecord`, new protocol, service +
|
||||
built-in adapters, SSH run logging, `_build_ssh_client`).
|
||||
- Deleted: `widgets/registry.py`.
|
||||
- Modified: `models/widgets.py` (service_id + widget_kind; `BuiltinWidgetKindInfo`).
|
||||
- Modified: `routers/widgets.py` (new validation, `/builtin`, data resolution).
|
||||
- Modified: `services/settings_store.py` (widget columns; no-op seeding).
|
||||
- Modified: `integrations/base.py` (`WidgetKind.config_model` for Pydantic
|
||||
widget-config validation).
|
||||
- Rewritten: `tests/test_widgets.py` (26 tests).
|
||||
|
||||
### Verification (Slice 2)
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
.venv/bin/ruff check . # All checks passed
|
||||
PYTHONPATH=src .venv/bin/python -m pytest # 225 passed
|
||||
PYTHONPATH=src .venv/bin/python -m pytest # 222 passed
|
||||
cd ../frontend
|
||||
npm run lint # 0 errors
|
||||
npm run build # success
|
||||
```
|
||||
|
||||
Smoke: encryption round-trip OK; missing `MANAGE_ENCRYPTION_KEY` raises on startup.
|
||||
### Known transient state (resolved by Slice 3)
|
||||
|
||||
## 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.
|
||||
Slice 2 is a backend-only breaking change to the widget API. Until Slice 3
|
||||
lands, the frontend still calls the removed `/api/widgets/types` and
|
||||
`/api/widgets/sources` endpoints and uses the old `widget_type` shape, so the
|
||||
dashboard widget config UI is non-functional at runtime. Build/lint stay green.
|
||||
This is the accepted transient state for a stacked backend→frontend rebind.
|
||||
|
||||
## 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.
|
||||
- Slice 3: Frontend services runtime (types, API, hooks, frontend service
|
||||
registry, service pages, route swap, remove addon pages, reconcile widget UI).
|
||||
- Slice 4: Dashboard picker on services, settings rework, remove
|
||||
`grafana_url`/`prometheus_url` env vars, docs + changelog.
|
||||
|
||||
Reference in New Issue
Block a user