# Apply Progress: Runtime Service Registry **Change:** `service-registry` **Apply run:** PR 1 + PR 2 / Slice 1 + Slice 2 **Date:** 2026-06-19 ## Slice 1 — Backend service foundation (MERGED) 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. ## Slice 2 — Backend widget rebind to services (this PR) ### Completed tasks - [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). ### Decision resolved mid-slice 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`. ### 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 # 222 passed cd ../frontend npm run lint # 0 errors npm run build # success ``` ### Known transient state (resolved by Slice 3) 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 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.