# SDD Explore: Configurable Dashboard Widgets

**Change:** `configurable-dashboard-widgets`  
**Phase:** explore  
**Date:** 2026-06-19

## 1. Existing Frontend Architecture

### Routing & navigation

- `frontend/src/App.tsx` defines a static `navItems` array and registers routes inside `<Routes>`.
- Current top-level pages: `/` Dashboard, `/observability`, `/media`, `/files`, `/backups`, `/users`, `/actions`, `/settings`.
- Sidebar and mobile drawer both consume `navItems`; adding a new addon page requires editing this file today.

### Page structure

- Pages live in `frontend/src/pages/`.
- Some pages are re-exported through thin entrypoints (`FileBrowser.tsx`, `Users.tsx`) while implementations live in `*.impl.tsx` files.
- `BackupsPage` and `ObservabilityPage` live under `frontend/src/components/` but are routed as pages.

### Dashboard composition today

- `frontend/src/pages/Dashboard.tsx` renders three hard-coded sections:
  1. **Shortcuts** — `SectionCard` + `ShortcutCard` grid.
  2. **Jellyfin activity** — `SectionCard` + `NowPlaying`.
  3. **Backups** — `BackupDashboardWidget`.
- Machine selection (e.g., active Jellyfin machine) is local component state.

## 2. Existing Backend Architecture

### Router registration

- `backend/src/media_library_viewer_api/main.py` statically imports routers and calls `app.include_router(...)`.
- Existing routers: `dashboard`, `monitoring`, `media`, `files`, `jobs`, `users`, `tasks`, `settings`, `backups`.

### Settings persistence

- `backend/src/media_library_viewer_api/services/settings_store.py` is the single SQLite-backed store.
- Pattern: `init_schema()` creates tables, JSON columns store flexible config, CRUD helpers return plain dicts.
- Already stores: monitoring machines, SSH keys, saved tasks, dashboard shortcuts, backup jobs/runs/alerts.

### Client resolution

- `backend/src/media_library_viewer_api/dependencies.py` resolves machines by `machine_id` query param and service tag.
- Jellyfin/SSH/local clients are built from machine config + SSH key store.

## 3. Widget / Addon Extension Points

### Frontend

| Extension point | Current state | How to reuse/extend |
|---|---|---|
| Sidebar nav | Static `navItems` | Derive from an addon registry; add dynamic `Route` entries |
| Dashboard surface | Hard-coded sections | Render widget instances from persisted config |
| Widget chrome | `SectionCard`, `MetricCard` | Reuse as container tiles |
| Page chrome | `ObservabilityPage` pattern | Model addon pages on shadcn Card + lucide icons + TanStack Query |
| Data fetching | `useDashboard`, `useBackups`, `useObservability` | Add `useWidgets` hooks per source |

### Backend

| Extension point | Current state | How to reuse/extend |
|---|---|---|
| Router registration | Static imports | Add a `widgets` dispatcher router or explicitly register addon routers |
| Persistence | `SettingsStore` JSON columns | Add `dashboard_widgets` / `addon_configs` tables |
| Client/credential access | `dependencies.py` machine resolution | Widget adapters reuse existing clients |
| Source adapters | None | New abstraction: `WidgetSource` per source type |

## 4. What a Widget Needs to Consume Data

### Source adapters (backend)

A widget source adapter should implement a small interface, e.g.:

```python
class WidgetSource(Protocol):
    source_type: str

    async def fetch(self, config: dict[str, Any]) -> dict[str, Any]:
        ...
```

Candidate source types:

- `jellyfin` — reuse `JellyfinClient` for counts/sessions.
- `backups` — reuse backup summary logic already in `dashboard.py`.
- `grafana` — link/iframe metadata or query a Grafana datasource (env URL/auth already configured).
- `prometheus` — instant query via env Prometheus URL.
- `alertmanager` — summary already exists in `monitoring.py`.
- `ssh_task` / `script` — run a saved task or whitelisted script through the existing machine/task registry.
- `static` — simple text/markdown/no-data widget.

### Config schema

Each widget instance needs:

- `id`, `addon_id`, `widget_type`, `title`, `icon`, `enabled`
- `source_type` + `source_config` (JSON)
- `refresh_interval_seconds`
- `layout` (position, size) or `sort_order`
- `display_options` (e.g., show header, variant)

### Refresh / polling

- Frontend: TanStack Query `refetchInterval` per widget type.
- Backend: short-lived proxy/adapters; avoid heavy polling for slow sources (SSH scripts).

### Credential handling

- **Never store secrets in widget config.**
- Jellyfin/SSH: use machine registry + SSH key store.
- Grafana/Prometheus/Alertmanager: use backend env settings (`get_settings()`).

## 5. Key Architectural Decisions

### Widget registry: compile-time vs runtime

- **Compile-time** (simpler): a static map of `widget_type -> component` in the frontend and source adapters in the backend.
- **Runtime** (more “addon”): backend serves an addon manifest, frontend lazily loads component modules.
- **Recommendation**: start compile-time for Phase 1; keep the data model flexible for runtime manifests later.

### Addon manifest format

A minimal manifest could be:

```yaml
id: grafana-addon
name: Grafana
icon: Activity
page:
  route: /addons/grafana
  component: ./addons/grafana/GrafanaPage
widgets:
  - type: grafana-link
    name: Grafana Link
    component: ./addons/grafana/GrafanaLinkWidget
    source_type: grafana
    config_schema:
      - name: dashboardUid
        type: string
```

### Dashboard persistence model

- Store widget instances globally (like current shortcuts) in a new `dashboard_widgets` table:
  - `id TEXT PRIMARY KEY`
  - `addon_id TEXT`
  - `widget_type TEXT`
  - `title TEXT`
  - `config_json TEXT`
  - `enabled INTEGER`
  - `sort_order INTEGER`
  - `created_at`, `updated_at`
- Consider a `user_id` column later if multi-user config is needed.

### Layout

- **Option A**: keep the existing stacked `SectionCard` list (simple, mobile-safe, no new dependencies).
- **Option B**: adopt a grid library (e.g., `react-grid-layout`) for drag/resize.
- **Recommendation**: Option A for Phase 1 to respect the thin-dashboard aesthetic and review budget.

### Routing

- Addon pages under `/addons/{addon_id}` avoids collisions and keeps the namespace clean.
- Alternatively top-level routes if the UX demands it.

### Backend API surface

Proposed endpoints:

- `GET /api/widgets/sources` — list available source types.
- `GET /api/widgets/types` — list widget types per addon.
- `GET /api/widgets/instances` — persisted dashboard 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 widget data via source adapter.

### Admin vs user configuration

- Today there is no RBAC; Settings is implicitly admin.
- Widget configuration can live in Settings or a new “Dashboard settings” mode.
- Keep it simple: global config, editable by any authenticated user.

### Default widgets

- Seed new installs with the existing defaults: Jellyfin activity, Backup summary.
- This preserves today’s out-of-box dashboard while making it configurable.

### Error / loading states

- Reuse `Skeleton`, `Alert`, `EmptyState` patterns from `ObservabilityPage`.
- Each widget fails independently; the dashboard continues to render.

## 6. Patterns to Reuse

- **UI containers**: `SectionCard`, `MetricCard`, `Card`, `Badge`.
- **Data fetching**: TanStack Query hooks with `refetchInterval`.
- **Local state**: `usePersistentState`.
- **Backend persistence**: `SettingsStore` JSON-column CRUD.
- **Dependency injection**: FastAPI `Depends` + machine/client resolution.
- **Type contracts**: Pydantic models in `backend/src/media_library_viewer_api/models/`.
- **Lazy loading**: `React.lazy` for optional addon frontends.

## 7. Open Questions for Proposal

1. Should Phase 1 support runtime addon discovery, or a closed built-in widget set?
2. Do we need a grid layout with drag/resize, or is the existing stacked SectionCard list sufficient?
3. Should widget configuration be global or per-user?
4. Which sources are in Phase 1? (Recommended: Jellyfin, Backups, Grafana link, Prometheus instant query, SSH saved task.)
5. Do we want addon pages to be iframes (e.g., Grafana) or custom React pages?
