# Apply Progress: Runtime Service Registry **Change:** `service-registry` **Apply run:** PR 1 + PR 2 + PR 3 (Slices 1–3) **Date:** 2026-06-19 ## Slice 1 — Backend service foundation (MERGED, PR #7) Fernet secrets, closed `integrations/` registry (Pydantic config + widget-config for grafana/prometheus/jellyfin/nextcloud/ssh_tasks), `services` + `service_task_runs` tables with cascade delete, `/api/services*` CRUD, `MANAGE_ENCRYPTION_KEY` required at startup. ## Slice 2 — Backend widget rebind (MERGED, PR #8) Widgets carry `service_id` + `widget_kind`; adapters take `fetch(service: ServiceRecord | None, widget_kind, config)`; backups + static stay as service-less built-ins; SSH adapter logs to `service_task_runs`; old `widgets/registry.py` retired; default seeding removed. ## Slice 3 — Frontend services runtime (this PR) ### Completed tasks - [x] 3.1 Service + new widget TypeScript types (`ServiceInstance`, `ServiceInstanceInput`, `ServiceTypeInfo`, `ServiceWidgetKindInfo`, `SecretFieldInfo`, `BuiltinWidgetKindInfo`; widget gains `service_id` + `widget_kind`). - [x] 3.2 Services API + hooks (`api/services.ts`, `hooks/useServices.ts`). Reconciled `api/widgets.ts` + `hooks/useWidgets.ts` to the new shape (removed sources/types; added builtin kinds). - [x] 3.3 Closed frontend service registry (`integrations/registry.ts`) mirroring the backend; `resolveWidget(widget, services)` maps a widget to its component + refresh interval. - [x] 3.4 Service page at `/services/:serviceType/:serviceId` with config view, empty-on-edit secret inputs + "set" badges, enable toggle, delete, and the service's widget-kind list. - [x] 3.5 Route swap: added `/services/:serviceType/:serviceId`; addon route retained for now (removed in Slice 4 cleanup). - [x] 3.6 Reconciled widget components to take `refreshIntervalMs` + `description` props; rewrote `WidgetConfigDialog` around the service → widget-kind picker (pulled 4.1 forward to keep the build whole). - [x] 3.7 Registry + Dashboard tests updated; new `integrations/registry.test.ts`. ### Decision resolved mid-slice Secret edit UX = **empty-on-edit + "set" badge** (blank = keep existing; typing = replace). Applied on the ServicePage secrets card. ### Files changed (Slice 3) - New: `api/services.ts`, `hooks/useServices.ts`, `integrations/registry.ts`, `integrations/registry.test.ts`, `pages/ServicePage.tsx`. - Modified: `types/index.ts`, `api/widgets.ts`, `hooks/useWidgets.ts`, `components/WidgetInstance.tsx`, `components/WidgetConfigDialog.tsx`, `pages/Dashboard.tsx`, `pages/__tests__/Dashboard.test.tsx`, `App.tsx`, all six `widgets/*.tsx` components, `widgets/index.ts`. - Deleted: `widgets/registry.ts`, `widgets/registry.test.ts`. ### Verification (Slice 3) ```bash cd frontend npm run lint # 0 errors npm run build # success npm run test # 70 passed cd ../backend .venv/bin/ruff check . # clean PYTHONPATH=src .venv/bin/python -m pytest # 222 passed ``` ### Deviations / notes - `WidgetConfigDialog` was rewritten in this slice (pulled forward from task 4.1) because the old dialog imported the deleted widget registry and would not compile. The SSH task-output widget keeps a dedicated task picker; other widget configs use a generic schema-driven field editor. - Addon pages (`/addons/:addonId`) are kept compiling but superseded by service pages; Slice 4 removes them and the now-unused machine Jellyfin/Jellyseerr fields + `grafana_url`/`prometheus_url` env vars, and writes the changelog. ## Remaining work - Slice 4: remove addon pages + machine app fields, remove `grafana_url`/`prometheus_url` from config + compose, docs + changelog (breaking upgrade note).