Files
manage/openspec/changes/archive/configurable-dashboard-widgets/exploration.md
Developer ca8927834e chore(openspec): archive completed changes
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.
2026-06-23 19:38:34 +00:00

8.2 KiB
Raw Permalink Blame History

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. ShortcutsSectionCard + ShortcutCard grid.
    2. Jellyfin activitySectionCard + NowPlaying.
    3. BackupsBackupDashboardWidget.
  • 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.:

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:

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?