Move finished change directories to openspec/changes/archive/: - configurable-dashboard-widgets - decommission-monitoring-poller - service-registry - unify-tasks-on-services All associated implementation has been merged to main.
8.8 KiB
SDD Proposal: Configurable Dashboard Widgets
Change: configurable-dashboard-widgets
Phase: proposal
Date: 2026-06-19
1. Problem / Why Now
The Manage dashboard (frontend/src/pages/Dashboard.tsx) currently renders three hard-coded sections: Shortcuts, Jellyfin activity, and Backups. Each new source of at-a-glance information requires editing the dashboard component and adding ad hoc backend endpoints. The user wants to surface information from many sources—Grafana, Jellyfin, SSH scripts, Prometheus, and more—without rebuilding the dashboard every time. We need a small, extensible widget system that makes the dashboard configurable while keeping the implementation within the existing FastAPI/React stack and the current thin-dashboard aesthetic.
2. Target Users and Situations
- Primary users: Homelab operators and small-team admins who open Manage to check overall system health.
- Workflow moments:
- First login of the day: scan backup status, Jellyfin activity, and key Prometheus metrics.
- Troubleshooting: jump from a widget into a dedicated addon page (e.g., Grafana dashboard, saved SSH task output).
- Onboarding a new machine: add a widget that exposes a saved SSH task or Prometheus query without a code change.
- Urgency: Medium. The existing dashboard already works; the pain is maintainability and visibility into an expanding set of sources.
3. Product Outcome
After Phase 1, an authenticated user can:
- See the existing dashboard sections rendered as configurable widgets.
- Add, edit, remove, enable/disable, and reorder widgets from a single global dashboard configuration.
- Choose from a built-in set of widget types backed by Jellyfin, backup summaries, Grafana deep-links, Prometheus instant queries, and SSH saved-task output.
- Open dedicated addon pages under
/addons/{addon_id}for widgets that need more space (e.g., Grafana details). - Continue using the familiar stacked
SectionCardlayout on desktop and mobile.
4. Scope Boundaries (Phase 1) and Non-Goals
In scope for Phase 1
- A closed, compile-time widget registry in both frontend and backend.
- Five source types:
jellyfin— activity/counts (reuses existinguseActivity/ counts data).backups— backup summary (reusesBackupDashboardWidgetlogic).grafana— deep-link to a Grafana dashboard or panel.prometheus— instant query result rendered as a metric or spark value.ssh_task— output of a saved task (reuses saved task registry andrun_task).
- Optional
statictext/markdown widget to dog-food the configuration UI. - Global dashboard widget config persisted in SQLite and editable by any authenticated user.
- Addon pages rendered as custom React pages under
/addons/{addon_id}; Grafana widgets deep-link to Grafana instead of embedding. - Stacked
SectionCardlayout; no grid, drag, or resize.
Non-goals (explicitly out of scope)
- Runtime addon discovery or dynamic component loading.
- Per-user widget configuration.
- Grid/drag/resize layout engine.
- Iframe embedding of Grafana or any other external UI.
- Public/unauthenticated widget access.
- Generic "run any script" widget; only saved tasks from the existing registry are allowed.
- Real-time WebSocket updates; polling via TanStack Query refetch intervals is sufficient.
5. High-Level Approach
5.1 Backend
-
Data model
-
Add a
dashboard_widgetstable inSettingsStore:CREATE TABLE dashboard_widgets ( id TEXT PRIMARY KEY, addon_id TEXT NOT NULL, widget_type TEXT NOT NULL, title TEXT NOT NULL, config_json TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, sort_order INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE INDEX idx_dashboard_widgets_sort ON dashboard_widgets(sort_order); -
config_jsonstores source-specific settings (e.g.,machine_id,dashboard_uid,promql,task_id). No secrets are stored here.
-
-
Widget source adapters
-
Introduce a small protocol/interface, e.g.
WidgetSource:class WidgetSource(Protocol): source_type: str async def fetch(self, config: dict[str, Any]) -> dict[str, Any]: ... -
Implement one adapter per source type. Adapters reuse existing dependency-injection helpers (
get_jellyfin_client,get_ssh_client, saved task registry, Grafana/Prometheus URLs fromget_settings()).
-
-
API surface
GET /api/widgets/sources— list available source types.GET /api/widgets/types— list built-in widget types per addon.GET /api/widgets/instances— persisted widget instances.POST /api/widgets/instances— create instance.PUT /api/widgets/instances/{id}— update instance.DELETE /api/widgets/instances/{id}— delete instance.GET /api/widgets/instances/{id}/data— fetch data via the source adapter.
-
Default data
- On first install, seed
dashboard_widgetswith the existing defaults: Jellyfin activity and Backup summary. Existing dashboards keep their current behavior after upgrade.
- On first install, seed
5.2 Frontend
-
Widget registry
- A static TypeScript map:
widget_type -> { component, defaultConfig, configSchema }. - Components render inside the existing
SectionCardcontainer and useMetricCard,Skeleton,Alert, andBadgepatterns already present inObservabilityPage.
- A static TypeScript map:
-
Dashboard rendering
Dashboard.tsxreplaces its three hard-coded sections with a loop over widget instances returned byuseWidgetsInstances().- Each widget fetches its own data through
useWidgetData(widgetId, refreshInterval)with TanStack QueryrefetchInterval.
-
Configuration UI
- Add an "Edit dashboard" action that opens a dialog/panel listing widget instances.
- Reuse the form patterns from
ShortcutDialogandSettings.tsxfor add/edit widget forms. - Source-specific fields are rendered by small config sub-forms registered next to each widget type.
-
Addon pages
- Register a wildcard-ish route
/addons/:addonIdinApp.tsxthat renders anAddonPagecomponent. AddonPagelooks up the addon in a static map and renders its dedicated page component (e.g.,GrafanaAddonPage).- Sidebar/nav items for addons are added to the existing
navItemsarray in Phase 1; dynamic nav is deferred to a future phase.
- Register a wildcard-ish route
5.3 Type contracts
- Add Pydantic models in
backend/src/media_library_viewer_api/models/forWidgetInstance,WidgetInstanceInput,WidgetTypeInfo,WidgetDataResponse. - Add matching TypeScript interfaces in
frontend/src/types/index.ts.
6. Success Criteria / Acceptance Criteria
- A fresh install shows the Jellyfin activity and Backup summary widgets by default.
- An authenticated user can add, edit, enable/disable, delete, and reorder widgets; changes persist across reloads.
- All five Phase 1 source types can be selected and rendered without errors when configured correctly.
- A misconfigured widget fails gracefully: the rest of the dashboard renders, and the widget shows an error state.
- Addon page route
/addons/{addon_id}renders a custom React page for the selected addon. - Existing backend tests and frontend typecheck (
npm run build) continue to pass. - No secrets are stored in
config_json.
7. Risks and Mitigations
| Risk | Mitigation |
|---|---|
| Scope creep toward a full grid/layout engine | Document and enforce Phase 1 non-goals; keep stacked SectionCard layout. |
| Widget source adapters duplicating backend logic | Reuse existing routers/clients via dependency injection rather than reimplementing endpoints. |
| Slow SSH-task widgets blocking dashboard renders | Fetch each widget independently; short timeouts; display loading/error states per widget. |
| Secrets leaking into widget config | Validate config schema server-side; reject credential fields; rely on machine/SSH key store and env settings. |
| Upgrade path breaks existing dashboards | Seed default widget rows on first install only; leave existing shortcuts/sections untouched. |
| Review budget overrun (~400 changed lines) | Keep the registry closed and compile-time; avoid generic schema editors; defer dynamic routing. |
8. Future Phases
- Per-user dashboards — add
user_idcolumn and UI toggle between global and personal layouts. - Runtime addon discovery — backend serves an addon manifest; frontend lazily loads addon page modules.
- Grid layout — optional
react-grid-layoutintegration with drag/resize behind a feature flag. - Additional sources — Alertmanager summary, Loki log snippets, custom HTTP endpoints, Jellyseerr requests.
- Widget templates/export — import/export widget layouts and shareable presets.