# 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`:

     ```sql
     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`:

     ```python
     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.
