Files
Developer 200d319fb0 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.
2026-06-19 20:07:47 +00:00

211 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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?