Three reusable-widget follow-up fixes:
1. Reference sort_order independently reorderable. Reordering a
referenced widget now updates the widget_references.sort_order (per-
dashboard), not the shared widget instance sort_order. New backend
update_widget_reference method + PUT /api/widgets/references/{id}
endpoint. Frontend moveInstance checks _ref_id to choose the right
mutation (updateRef for references, saveWidget for owned).
2. Detach preserves service_id. detach_widget_reference now copies the
original widget's service_id into the clone, so service-bound widgets
(Grafana chart, Jellyfin activity) continue to render after detach.
3. Named dashboards support widget references. NamedDashboardPage
fetches useWidgetReferences('named:<slug>') and renders them via
WidgetInstanceCard alongside pinned links. 'Edit widgets' button
opens WidgetConfigDialog with dashboardScope='named:<slug>'.
Also: removed useMemo on combinedWidgets in WidgetConfigDialog to fix
a react-hooks/preserve-manual-memoization lint error (the React Compiler
ESLint plugin couldn't verify the spread+sort memoization).
283 backend tests pass (+1 update_reference test); 128 frontend tests
pass; ruff clean; 0 lint errors.
Widgets configured on one dashboard (e.g., a Grafana service's Overview)
can now be live-referenced on other dashboards. Editing the widget config
updates it everywhere it's referenced. References can be detached into
independent clones.
Backend: new widget_references table (dashboard_scope, widget_id,
sort_order) with ON DELETE CASCADE. CRUD methods + 4 endpoints:
GET/POST /api/widgets/references, DELETE /api/widgets/references/{id},
POST /api/widgets/references/{id}/detach (clones the widget into a
standalone instance, then removes the reference).
Frontend: WidgetConfigDialog gains a dashboardScope prop. When set
(the main Dashboard passes 'main'), the dialog shows:
- Owned + referenced widgets in a combined list, with a link badge on
references.
- 'Add existing widget' picker: searchable list of ALL widget instances
not already on this dashboard. Click to create a reference.
- Detach button on references: clones the widget (service_id=NULL) and
removes the reference.
- Delete on a reference removes the REFERENCE (not the original widget).
Dashboard renders referenced widgets alongside owned widgets.
Detaching a service-bound widget clones it with service_id=NULL — the
clone may need re-binding to a service to render correctly. Named
dashboards don't pass dashboardScope yet (pinned-links-only); when they
gain widget support, the backend already handles any scope string.
282 backend tests pass (+2 reference lifecycle); 127 frontend tests
pass; ruff/eslint/tsc/vite all green.
Four coupled changes to the services-as-hub IA:
1. Nav entries use service TYPE names (Jellyfin, SSH Tasks, Alertmanager,
Grafana, Prometheus, Backups, Authentik) instead of conceptual names
(Media, Files, Actions, Alerts, Users). ssh_tasks collapses to one
entry ('SSH Tasks') instead of two. The content tabs inside each
service page surface the concepts (Files, Actions).
2. Service page gains a two-level tab structure when multiple enabled
instances of the same type exist: instance tabs on top ([Main Jellyfin]
[Backup Jellyfin]), content tabs below ([Overview] [Media] [Requests]
[Widgets]). Clicking an instance tab navigates to the sibling's route.
Single instance: no instance tabs. Replaces the dropdown switcher.
3. Config tab (connection fields, secrets, enable/disable, delete) moves
from the service page to Settings > Services tab. The service page
becomes a PURE operational view (Overview + content tabs + Widgets) --
no save/delete/config state. Settings gains a 4th tab 'Services' with
ServiceConfigEditor per instance (schema-driven config fields, secrets
with leave-blank-to-keep semantics, ConfirmDialog on delete).
4. Overview tab is now a configurable widget grid per service instance.
Each instance manages its own set of widgets on its Overview. Backend
widget list endpoints gain ?service_id= and ?scope= (dashboard|service)
filter params; the main Dashboard uses scope=dashboard to exclude
service-scoped widgets. The OverviewTab reuses WidgetInstanceCard +
WidgetConfigDialog. Empty state CTA for instances with no widgets.
All service-tab stubs are replaced; stubs.tsx deleted.
272 backend tests pass (+1 widget filter); 121 frontend tests pass (+3
instance-tabs + OverviewTab); lint/build green both sides.
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.