200d319fb0
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.
8.2 KiB
8.2 KiB
SDD Explore: Configurable Dashboard Widgets
Change: configurable-dashboard-widgets
Phase: explore
Date: 2026-06-19
1. Existing Frontend Architecture
Routing & navigation
frontend/src/App.tsxdefines a staticnavItemsarray 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.tsxfiles. BackupsPageandObservabilityPagelive underfrontend/src/components/but are routed as pages.
Dashboard composition today
frontend/src/pages/Dashboard.tsxrenders three hard-coded sections:- Shortcuts —
SectionCard+ShortcutCardgrid. - Jellyfin activity —
SectionCard+NowPlaying. - Backups —
BackupDashboardWidget.
- Shortcuts —
- Machine selection (e.g., active Jellyfin machine) is local component state.
2. Existing Backend Architecture
Router registration
backend/src/media_library_viewer_api/main.pystatically imports routers and callsapp.include_router(...).- Existing routers:
dashboard,monitoring,media,files,jobs,users,tasks,settings,backups.
Settings persistence
backend/src/media_library_viewer_api/services/settings_store.pyis 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.pyresolves machines bymachine_idquery 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.:
class WidgetSource(Protocol):
source_type: str
async def fetch(self, config: dict[str, Any]) -> dict[str, Any]:
...
Candidate source types:
jellyfin— reuseJellyfinClientfor counts/sessions.backups— reuse backup summary logic already indashboard.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 inmonitoring.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,enabledsource_type+source_config(JSON)refresh_interval_secondslayout(position, size) orsort_orderdisplay_options(e.g., show header, variant)
Refresh / polling
- Frontend: TanStack Query
refetchIntervalper 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 -> componentin 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:
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_widgetstable:id TEXT PRIMARY KEYaddon_id TEXTwidget_type TEXTtitle TEXTconfig_json TEXTenabled INTEGERsort_order INTEGERcreated_at,updated_at
- Consider a
user_idcolumn later if multi-user config is needed.
Layout
- Option A: keep the existing stacked
SectionCardlist (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,EmptyStatepatterns fromObservabilityPage. - 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:
SettingsStoreJSON-column CRUD. - Dependency injection: FastAPI
Depends+ machine/client resolution. - Type contracts: Pydantic models in
backend/src/media_library_viewer_api/models/. - Lazy loading:
React.lazyfor optional addon frontends.
7. Open Questions for Proposal
- Should Phase 1 support runtime addon discovery, or a closed built-in widget set?
- Do we need a grid layout with drag/resize, or is the existing stacked SectionCard list sufficient?
- Should widget configuration be global or per-user?
- Which sources are in Phase 1? (Recommended: Jellyfin, Backups, Grafana link, Prometheus instant query, SSH saved task.)
- Do we want addon pages to be iframes (e.g., Grafana) or custom React pages?