# 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 ``. - 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?