feat(widgets): add backend CRUD, registry, and default seeding

Introduce a closed, compile-time widget registry and backend CRUD for
dashboard widget instances.

- Add dashboard_widgets SQLite table in SettingsStore with CRUD helpers and
  default seeding (Jellyfin + Backups) on first install.
- Add Pydantic models with credential-key and secret-value rejection.
- Add widgets router: /api/widgets/sources, /types, /instances CRUD.
- Call ensure_defaults() in app lifespan so fresh installs seed defaults.
- Add backend tests covering registry, CRUD, validation, and seeding.
- Include SDD artifacts: exploration, proposal, spec, design, tasks.
This commit is contained in:
Developer
2026-06-19 20:07:47 +00:00
parent 24427b4869
commit 200d319fb0
13 changed files with 2894 additions and 6 deletions
@@ -0,0 +1,210 @@
# 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 todays 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?