feat(services): frontend services runtime and widget rebind
PR 3 of 4 for the runtime service registry change. - Add service + new-shape widget TypeScript types; widgets carry service_id + widget_kind (service-bound) or null (built-in). - Add services API client + TanStack Query hooks; reconcile the widget API client/hooks to the new endpoints (remove sources/types; add builtin kinds). - Add closed frontend service registry (integrations/registry.ts) mirroring the backend, with resolveWidget(widget, services) mapping a widget to its component + refresh interval. - Add ServicePage at /services/:serviceType/:serviceId with config view, empty-on-edit secret inputs + 'set' badges, enable toggle, delete, and the service's widget-kind list. - Register /services/:serviceType/:serviceId in App.tsx. - Reconcile the six widget components to refreshIntervalMs + description props; rewrite WidgetConfigDialog around a service -> widget-kind picker. - Update Dashboard test; add integrations/registry.test.ts. Verification: frontend lint 0 errors, build success, 70 tests passed; backend ruff clean, 222 tests passed.
This commit is contained in:
@@ -1,78 +1,87 @@
|
||||
# Apply Progress: Runtime Service Registry
|
||||
|
||||
**Change:** `service-registry`
|
||||
**Apply run:** PR 1 + PR 2 / Slice 1 + Slice 2
|
||||
**Apply run:** PR 1 + PR 2 + PR 3 (Slices 1–3)
|
||||
**Date:** 2026-06-19
|
||||
|
||||
## Slice 1 — Backend service foundation (MERGED)
|
||||
## Slice 1 — Backend service foundation (MERGED, PR #7)
|
||||
|
||||
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.
|
||||
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 to services (this PR)
|
||||
## 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] 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).
|
||||
- [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
|
||||
|
||||
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`.
|
||||
Secret edit UX = **empty-on-edit + "set" badge** (blank = keep existing; typing
|
||||
= replace). Applied on the ServicePage secrets card.
|
||||
|
||||
### Files changed (Slice 2)
|
||||
### Files changed (Slice 3)
|
||||
|
||||
- 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).
|
||||
- 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 2)
|
||||
### Verification (Slice 3)
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
.venv/bin/ruff check . # All checks passed
|
||||
PYTHONPATH=src .venv/bin/python -m pytest # 222 passed
|
||||
cd ../frontend
|
||||
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
|
||||
```
|
||||
|
||||
### Known transient state (resolved by Slice 3)
|
||||
### Deviations / notes
|
||||
|
||||
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.
|
||||
- `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 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.
|
||||
- Slice 4: remove addon pages + machine app fields, remove
|
||||
`grafana_url`/`prometheus_url` from config + compose, docs + changelog
|
||||
(breaking upgrade note).
|
||||
|
||||
Reference in New Issue
Block a user