ca8927834e
Move finished change directories to openspec/changes/archive/: - configurable-dashboard-widgets - decommission-monitoring-poller - service-registry - unify-tasks-on-services All associated implementation has been merged to main.
211 lines
8.2 KiB
Markdown
211 lines
8.2 KiB
Markdown
# 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?
|