Files
manage/openspec/changes/archive/configurable-dashboard-widgets/proposal.md
Developer ca8927834e chore(openspec): archive completed changes
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.
2026-06-23 19:38:34 +00:00

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 SectionCard layout 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:
    1. jellyfin — activity/counts (reuses existing useActivity / counts data).
    2. backups — backup summary (reuses BackupDashboardWidget logic).
    3. grafana — deep-link to a Grafana dashboard or panel.
    4. prometheus — instant query result rendered as a metric or spark value.
    5. ssh_task — output of a saved task (reuses saved task registry and run_task).
  • Optional static text/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 SectionCard layout; 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

  1. Data model

    • Add a dashboard_widgets table in SettingsStore:

      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_json stores source-specific settings (e.g., machine_id, dashboard_uid, promql, task_id). No secrets are stored here.

  2. 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 from get_settings()).

  3. 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.
  4. Default data

    • On first install, seed dashboard_widgets with the existing defaults: Jellyfin activity and Backup summary. Existing dashboards keep their current behavior after upgrade.

5.2 Frontend

  1. Widget registry

    • A static TypeScript map: widget_type -> { component, defaultConfig, configSchema }.
    • Components render inside the existing SectionCard container and use MetricCard, Skeleton, Alert, and Badge patterns already present in ObservabilityPage.
  2. Dashboard rendering

    • Dashboard.tsx replaces its three hard-coded sections with a loop over widget instances returned by useWidgetsInstances().
    • Each widget fetches its own data through useWidgetData(widgetId, refreshInterval) with TanStack Query refetchInterval.
  3. Configuration UI

    • Add an "Edit dashboard" action that opens a dialog/panel listing widget instances.
    • Reuse the form patterns from ShortcutDialog and Settings.tsx for add/edit widget forms.
    • Source-specific fields are rendered by small config sub-forms registered next to each widget type.
  4. Addon pages

    • Register a wildcard-ish route /addons/:addonId in App.tsx that renders an AddonPage component.
    • AddonPage looks 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 navItems array in Phase 1; dynamic nav is deferred to a future phase.

5.3 Type contracts

  • Add Pydantic models in backend/src/media_library_viewer_api/models/ for WidgetInstance, WidgetInstanceInput, WidgetTypeInfo, WidgetDataResponse.
  • Add matching TypeScript interfaces in frontend/src/types/index.ts.

6. Success Criteria / Acceptance Criteria

  1. A fresh install shows the Jellyfin activity and Backup summary widgets by default.
  2. An authenticated user can add, edit, enable/disable, delete, and reorder widgets; changes persist across reloads.
  3. All five Phase 1 source types can be selected and rendered without errors when configured correctly.
  4. A misconfigured widget fails gracefully: the rest of the dashboard renders, and the widget shows an error state.
  5. Addon page route /addons/{addon_id} renders a custom React page for the selected addon.
  6. Existing backend tests and frontend typecheck (npm run build) continue to pass.
  7. 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

  1. Per-user dashboards — add user_id column and UI toggle between global and personal layouts.
  2. Runtime addon discovery — backend serves an addon manifest; frontend lazily loads addon page modules.
  3. Grid layout — optional react-grid-layout integration with drag/resize behind a feature flag.
  4. Additional sources — Alertmanager summary, Loki log snippets, custom HTTP endpoints, Jellyseerr requests.
  5. Widget templates/export — import/export widget layouts and shareable presets.