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:
@@ -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 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?
|
||||
Reference in New Issue
Block a user