Files
manage/openspec/changes/archive/configurable-dashboard-widgets/tasks.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

13 KiB
Raw Permalink Blame History

SDD Tasks: Configurable Dashboard Widgets

Change: configurable-dashboard-widgets
Phase: tasks
Date: 2026-06-19

Review Workload Forecast

Field Value
Estimated changed lines ~1,5501,650 (sum of four implementation slices)
400-line budget risk High
Chained PRs recommended Yes
Suggested split PR 1: Backend CRUD + default seeding → PR 2: Backend source adapters + data endpoint → PR 3: Frontend types/API/hooks/registry/components → PR 4: Dashboard loop + config UI + addon pages
Delivery strategy ask-on-risk
Chain strategy stacked-to-main
Decision needed before apply: Yes
Chained PRs recommended: Yes
Chain strategy: stacked-to-main
400-line budget risk: High

Note: The preflight preference is single-PR-default, but the Phase 1 implementation clearly exceeds the ~400 changed-line review budget. The recommended split above keeps every slice independently testable and green. Confirm the chained-PR strategy before moving to sdd-apply.


Phase 1 Goal

Replace the hard-coded dashboard sections in frontend/src/pages/Dashboard.tsx with a persisted, closed-registry widget system. Backend stores widget instances in SQLite, exposes CRUD + per-widget data endpoints, and provides source adapters for Jellyfin, backups, Grafana links, Prometheus instant queries, saved SSH tasks, and static text. Frontend renders enabled widgets in sort order, fetches data independently, and provides a configuration UI plus /addons/:addonId pages.


Slice 1: Backend CRUD and default seeding

Goal: Persist widget instances and expose registry metadata + CRUD endpoints. Leave all source adapters and data fetch for Slice 2.

  • 1.1 Create widget Pydantic models

    • Files: backend/src/media_library_viewer_api/models/widgets.py (new)
    • Lines: ~70
    • Dependencies: none
    • Details: Add WidgetInstance, WidgetInstanceInput, WidgetTypeInfo, WidgetDataResponse. Include credential-key validator (password, token, secret, api_key, etc.) and secret-looking-value heuristic.
  • 1.2 Create backend widget registry

    • Files: backend/src/media_library_viewer_api/widgets/__init__.py (new), backend/src/media_library_viewer_api/widgets/registry.py (new)
    • Lines: ~50
    • Dependencies: 1.1
    • Details: Define WIDGET_REGISTRY mapping widget_typeaddon_id, name, description, source_type, JSON Schema config_schema for all six Phase 1 types.
  • 1.3 Implement widgets router (CRUD + metadata)

    • Files: backend/src/media_library_viewer_api/routers/widgets.py (new)
    • Lines: ~110
    • Dependencies: 1.1, 1.2
    • Details: Implement GET /api/widgets/sources, GET /api/widgets/types, GET /api/widgets/instances, POST /api/widgets/instances (201), PUT /api/widgets/instances/{id}, DELETE /api/widgets/instances/{id}. Validate widget_type and addon_id against registry; validate config schema; reject credential keys.
  • 1.4 Extend SettingsStore for dashboard_widgets

    • Files: backend/src/media_library_viewer_api/services/settings_store.py
    • Lines: ~90
    • Dependencies: none
    • Details: Add table + index idx_dashboard_widgets_sort, _row_to_widget, _normalize_widget_payload, list_widgets, get_widget, upsert_widget, delete_widget, and _seed_dashboard_widgets (Jellyfin + Backups defaults only when table is empty).
  • 1.5 Register widgets router in main.py

    • Files: backend/src/media_library_viewer_api/main.py
    • Lines: ~5
    • Dependencies: 1.3
    • Details: app.include_router(widgets_router.router); endpoints inherit existing JWT/API-key middleware.
  • 1.6 Add backend tests for registry, CRUD, and seeding

    • Files: backend/tests/test_widgets.py (new)
    • Lines: ~75
    • Dependencies: 1.3, 1.4
    • Details: Test sources/types lists, create/read/update/delete, unknown widget type → 422, credential key → 422, fresh-store seeding, existing store not re-seeded.
  • 1.7 Verify backend slice

    • Run: cd backend && ruff check . && PYTHONPATH=src pytest tests/test_widgets.py

Slice 1 total: ~400 changed lines.


Slice 2: Backend source adapters and data endpoint

Goal: Fetch widget data through stateless adapters reusing existing DI and clients.

  • 2.1 Add observability URL settings

    • Files: backend/src/media_library_viewer_api/config.py
    • Lines: ~15
    • Dependencies: none
    • Details: Add grafana_url: str = "http://grafana:3000" and prometheus_url: str = "http://prometheus:9090".
  • 2.2 Create source adapters

    • Files: backend/src/media_library_viewer_api/widgets/sources.py (new)
    • Lines: ~200
    • Dependencies: 1.2, 2.1
    • Details: Implement WidgetSource protocol + adapters for jellyfin, backups, grafana, prometheus, ssh_task, static. Catch exceptions and return {"error": "..."}. Apply per-type timeouts (10 s / 10 s / 5 s / 10 s / 30 s / none).
  • 2.3 Add per-widget data endpoint

    • Files: backend/src/media_library_viewer_api/routers/widgets.py
    • Lines: ~35
    • Dependencies: 1.3, 2.2
    • Details: Implement GET /api/widgets/instances/{id}/data, returning WidgetDataResponse with widget_id, widget_type, data, error, fetched_at. Unhandled adapter exceptions → 500.
  • 2.4 Share Jellyfin activity mapping helper

    • Files: backend/src/media_library_viewer_api/routers/dashboard.py, backend/src/media_library_viewer_api/domain/dashboard.py (new)
    • Lines: ~25
    • Dependencies: 2.2
    • Details: Move _map_sessions_to_activity_rows to domain/dashboard.py; import it from both routers/dashboard.py and the Jellyfin adapter.
  • 2.5 Add backend tests for adapters and data endpoint

    • Files: backend/tests/test_widgets.py
    • Lines: ~85
    • Dependencies: 2.2, 2.3
    • Details: Test static widget data round-trip, misconfigured jellyfin returns error with HTTP 200, SSH task adapter timeout returns error payload, unhandled exception path returns 500.
  • 2.6 Verify backend slice

    • Run: cd backend && ruff check . && PYTHONPATH=src pytest tests/test_widgets.py

Slice 2 total: ~360 changed lines.


Slice 3: Frontend types, API, hooks, registry, and widget components

Goal: Build the frontend widget runtime: types, API client, hooks, closed registry, and presentational components. No dashboard integration yet.

  • 3.1 Add TypeScript widget interfaces

    • Files: frontend/src/types/index.ts
    • Lines: ~45
    • Dependencies: none
    • Details: Add WidgetInstance, WidgetInstanceInput, WidgetTypeInfo, WidgetDataResponse, and WidgetSource union type with exact field names from the spec.
  • 3.2 Create widget API client

    • Files: frontend/src/api/widgets.ts (new)
    • Lines: ~60
    • Dependencies: 3.1
    • Details: Functions for fetchWidgetSources, fetchWidgetTypes, fetchWidgetInstances, createWidgetInstance, updateWidgetInstance, deleteWidgetInstance, fetchWidgetData.
  • 3.3 Create widget TanStack Query hooks

    • Files: frontend/src/hooks/useWidgets.ts (new)
    • Lines: ~70
    • Dependencies: 3.2
    • Details: useWidgetInstances, useWidgetData(widgetId, refreshInterval), useSaveWidgetInstance, useDeleteWidgetInstance, useWidgetSources, useWidgetTypes. Use correct per-type refetchInterval.
  • 3.4 Create frontend widget registry

    • Files: frontend/src/widgets/registry.ts (new)
    • Lines: ~70
    • Dependencies: 3.1
    • Details: Define WidgetConfigField, WidgetDefinition, WIDGET_REGISTRY for all six types, getWidgetDefinition, plus refreshInterval defaults.
  • 3.5 Implement widget presentational components

    • Files: frontend/src/widgets/JellyfinWidget.tsx, BackupsWidget.tsx, GrafanaLinkWidget.tsx, PrometheusMetricWidget.tsx, SshTaskWidget.tsx, StaticWidget.tsx
    • Lines: ~150
    • Dependencies: 3.1, 3.3, 3.4
    • Details: Each component receives widget: WidgetInstance and renders inside the existing card patterns. Grafana widget renders an external deep-link only (no iframe).
  • 3.6 Add frontend registry unit tests

    • Files: frontend/tests/widgets.test.mjs (new)
    • Lines: ~40
    • Dependencies: 3.4
    • Details: Assert registry contains exactly six widget types and refresh intervals match spec.
  • 3.7 Verify frontend slice

    • Run: cd frontend && npm run lint && npm run build

Slice 3 total: ~435 changed lines.


Slice 4: Dashboard loop, configuration UI, and addon pages

Goal: Wire widgets into the dashboard, add configuration UI, and add addon page routes.

  • 4.1 Refactor Dashboard.tsx to render widget instances

    • Files: frontend/src/pages/Dashboard.tsx
    • Lines: ~60
    • Dependencies: Slice 3
    • Details: Keep the existing Shortcuts section as a hard-coded first-class section (no migration). Add an "Edit dashboard" button. Render enabled widgets sorted by sort_order via <WidgetInstance />.
  • 4.2 Create widget instance renderer

    • Files: frontend/src/components/WidgetInstance.tsx (new)
    • Lines: ~40
    • Dependencies: 3.3, 3.4, 3.5
    • Details: Lookup definition, call useWidgetData, show skeleton on first load, render inline Alert for error, dispatch to registered component.
  • 4.3 Create widget configuration dialog

    • Files: frontend/src/components/WidgetConfigDialog.tsx (new)
    • Lines: ~160
    • Dependencies: 3.3, 3.4
    • Details: List all instances with enabled toggle, sort-order input, up/down reorder, edit/delete. Add widget flow selects type then renders source-specific config fields. Use existing shadcn Dialog, Input, Label, Switch, Select, Button, Alert.
  • 4.4 Create addon pages

    • Files: frontend/src/pages/AddonPage.tsx (new), frontend/src/addons/GrafanaAddonPage.tsx (new), frontend/src/addons/PrometheusAddonPage.tsx (new), frontend/src/addons/SshTasksAddonPage.tsx (new)
    • Lines: ~130
    • Dependencies: none
    • Details: AddonPage maps addonId to static page components; unknown addon shows an Alert. Pages render links/metadata only (no iframes).
  • 4.5 Register addon route in App.tsx

    • Files: frontend/src/App.tsx
    • Lines: ~5
    • Dependencies: 4.4
    • Details: Add <Route path="/addons/:addonId" element={<AddonPage />} /> in both the OIDC and non-OIDC route trees.
  • 4.6 Update docs/REQUIREMENTS.md

    • Files: docs/REQUIREMENTS.md
    • Lines: ~25
    • Dependencies: none
    • Details: Document configurable dashboard widgets, supported source types, security rule (no secrets in config), and addon pages.
  • 4.7 Verify frontend slice and full build

    • Run: cd frontend && npm run lint && npm run build

Slice 4 total: ~420 changed lines.


Integration and acceptance verification

  • 5.1 Backend full test run

    • Run: cd backend && PYTHONPATH=src pytest
    • Verify existing tests still pass and test_widgets.py covers registry, CRUD, seeding, and data fetch.
  • 5.2 Frontend full build + lint

    • Run: cd frontend && npm run lint && npm run build
    • Verify no TypeScript errors and no new lint failures.
  • 5.3 Manual dev-stack verification

    • Run: docker compose -f docker-compose.dev.yml up --build
    • Verify:
      • Fresh install shows Jellyfin activity + Backups widgets.
      • Disabled widget is hidden.
      • Reorder changes dashboard order.
      • Misconfigured widget shows inline error without blocking dashboard.
      • /addons/grafana, /addons/prometheus, /addons/ssh-tasks render; unknown addon shows not-found alert.
      • No widget config can contain api_key, token, secret, etc.

Total Phase 1 estimate

Slice Changed lines
Slice 1: Backend CRUD + seeding ~400
Slice 2: Backend adapters + data endpoint ~360
Slice 3: Frontend runtime (types/API/hooks/registry/components) ~435
Slice 4: Dashboard loop + config UI + addon pages ~420
Integration tests/docs ~25
Total ~1,640

This exceeds the ~400-line review budget. Use the four chained PRs above; each slice is independently buildable/testable and leaves the app functional.


Tests and docs summary

  • Backend tests: New backend/tests/test_widgets.py covering registry, CRUD, validation, default seeding, and adapter data fetch. Run with pytest.
  • Frontend tests: New frontend/tests/widgets.test.mjs covering registry contents and refresh intervals. Run implicitly via npm run build/lint; add Vitest/MSW tests only if the project adopts Vitest before this change.
  • Typecheck/build: npm run build (runs tsc -b) must pass for every slice.
  • Docs: Update docs/REQUIREMENTS.md to describe the widget system, security rule, and addon pages.

Guard lines

Decision needed before apply: Yes
Chained PRs recommended: Yes
Chain strategy: stacked-to-main
400-line budget risk: High