Files
manage/.pi-tmp/refine-4-out.md
T
Developer 8d2e4c9bfd Service IA refinement: nav naming, instance tabs, config to Settings, configurable Overview
Four coupled changes to the services-as-hub IA:

1. Nav entries use service TYPE names (Jellyfin, SSH Tasks, Alertmanager,
   Grafana, Prometheus, Backups, Authentik) instead of conceptual names
   (Media, Files, Actions, Alerts, Users). ssh_tasks collapses to one
   entry ('SSH Tasks') instead of two. The content tabs inside each
   service page surface the concepts (Files, Actions).

2. Service page gains a two-level tab structure when multiple enabled
   instances of the same type exist: instance tabs on top ([Main Jellyfin]
   [Backup Jellyfin]), content tabs below ([Overview] [Media] [Requests]
   [Widgets]). Clicking an instance tab navigates to the sibling's route.
   Single instance: no instance tabs. Replaces the dropdown switcher.

3. Config tab (connection fields, secrets, enable/disable, delete) moves
   from the service page to Settings > Services tab. The service page
   becomes a PURE operational view (Overview + content tabs + Widgets) --
   no save/delete/config state. Settings gains a 4th tab 'Services' with
   ServiceConfigEditor per instance (schema-driven config fields, secrets
   with leave-blank-to-keep semantics, ConfirmDialog on delete).

4. Overview tab is now a configurable widget grid per service instance.
   Each instance manages its own set of widgets on its Overview. Backend
   widget list endpoints gain ?service_id= and ?scope= (dashboard|service)
   filter params; the main Dashboard uses scope=dashboard to exclude
   service-scoped widgets. The OverviewTab reuses WidgetInstanceCard +
   WidgetConfigDialog. Empty state CTA for instances with no widgets.

All service-tab stubs are replaced; stubs.tsx deleted.

272 backend tests pass (+1 widget filter); 121 frontend tests pass (+3
instance-tabs + OverviewTab); lint/build green both sides.
2026-06-26 22:25:46 +00:00

7.3 KiB

Configurable per-service Overview (change 4)

Files changed (10 files, ~310 lines)

File Status Lines
backend/src/media_library_viewer_api/services/settings_store.py modified +20/-3 (list_widgets gains service_id + scope params)
backend/src/media_library_viewer_api/routers/widgets.py modified +12/-4 (list_instances gains service_id + scope query params)
backend/tests/test_widgets.py modified +36 (filter test)
frontend/src/api/widgets.ts modified +8/-1 (fetchWidgetInstances accepts serviceId? + scope?)
frontend/src/hooks/useWidgets.ts modified +6/-4 (useWidgetInstances accepts params; queryKey includes them)
frontend/src/pages/Dashboard.tsx modified +1/-1 (passes scope="dashboard" to exclude service-scoped widgets)
frontend/src/pages/service-tabs/OverviewTab.tsx new 67
frontend/src/pages/service-tabs/__tests__/OverviewTab.test.tsx new 79
frontend/src/pages/service-tabs/index.ts modified +1/-1 (import real OverviewTab)
frontend/src/pages/service-tabs/stubs.tsx deleted -19

Backend filter shape

GET /api/widgets/instances now accepts:

  • ?service_id=X — filter to widgets for service X
  • ?scope=dashboard — only NULL service_id widgets (main dashboard)
  • ?scope=service — only non-NULL service_id widgets

SettingsStore.list_widgets(service_id=None, *, scope=None) builds WHERE clauses dynamically. No-args returns all (backward-compatible).

OverviewTab structure

OverviewTab({ instance }):

  • Fetches useWidgetInstances(instance.id) (scoped to this service).
  • Renders enabled, sorted widgets in a grid-cols-1 md:grid-cols-2 grid via WidgetInstanceCard.
  • "Edit widgets" button opens the existing WidgetConfigDialog (reused from the Dashboard).
  • Empty state: "No widgets on this overview yet" + "Add widgets" button.
  • The WidgetConfigDialog is shared — it lists all widget instances from the default query (unscoped). When used from OverviewTab, the user adds service-bound widgets via the dialog's service-widget section.

Config dialog integration

Reuses the existing WidgetConfigDialog as-is. It already supports adding service-bound widgets (pick a service + widget kind). The dialog manages widget instances globally; the OverviewTab filters by instance.id. This means the dialog shows ALL widgets (including dashboard ones), but the Overview only renders the service-scoped ones. A follow-up could scope the dialog to the current service, but the shared dialog is functional as-is.

Validation

cd backend && .venv/bin/ruff check .                    → All checks passed!
cd backend && .venv/bin/python -m pytest tests/         → 272 passed, 2 warnings
cd frontend && npm run lint                             → 0 errors, 0 warnings
cd frontend && npm run build                            → ✓ built (tsc + vite)
cd frontend && npm run test                             → 36 files / 121 tests passed

Deviations

  1. WidgetConfigDialog is unscoped. It lists all widget instances. The OverviewTab filters by instance.id at render time, but the dialog shows everything. Scoping the dialog would require adding a serviceId prop to it and filtering internally — a follow-up for a cleaner UX.
  2. stubs.tsx deleted. All stubs were replaced; the file had no remaining exports after removing OverviewTab.
  3. ServicePage tests updated. Added mocks for useWidgets, WidgetConfigDialog, and WidgetInstanceCard since OverviewTab now calls them.

skill_resolution

none — no project/user SKILL.md paths were injected; no .atl/skill-registry.md found.

Residual risks

  • WidgetConfigDialog is shared and unscoped — adding a widget from the OverviewTab's edit button could add a dashboard widget that doesn't show on this overview.
  • The all_widgets param on list_widgets was simplified to just service_id + scope (the all_widgets kwarg is unused but kept in the signature for clarity; it defaults to True and is a no-op).
  • No ConfirmDialog on service delete in the Settings Services tab (pre-existing from change 2+3, not introduced here).
{
  "criteriaSatisfied": [
    {
      "id": "criterion-1",
      "status": "satisfied",
      "evidence": "Implements configurable per-service Overview (widget grid scoped by instance.id) + backend filter params (?service_id= + ?scope=) + Dashboard scope fix + tests. No scope widening: 10 files, ~310 lines. 272 backend + 121 frontend tests pass; lint/build green both sides."
    }
  ],
  "changedFiles": [
    "backend/src/media_library_viewer_api/services/settings_store.py",
    "backend/src/media_library_viewer_api/routers/widgets.py",
    "backend/tests/test_widgets.py",
    "frontend/src/api/widgets.ts",
    "frontend/src/hooks/useWidgets.ts",
    "frontend/src/pages/Dashboard.tsx",
    "frontend/src/pages/service-tabs/OverviewTab.tsx",
    "frontend/src/pages/service-tabs/__tests__/OverviewTab.test.tsx",
    "frontend/src/pages/service-tabs/index.ts",
    "frontend/src/pages/service-tabs/stubs.tsx"
  ],
  "testsAddedOrUpdated": [
    "backend/tests/test_widgets.py",
    "frontend/src/pages/service-tabs/__tests__/OverviewTab.test.tsx",
    "frontend/src/pages/__tests__/ServicePage.test.tsx"
  ],
  "commandsRun": [
    {
      "command": "cd backend && .venv/bin/ruff check .",
      "result": "passed",
      "summary": "All checks passed"
    },
    {
      "command": "cd backend && .venv/bin/python -m pytest tests/ -q",
      "result": "passed",
      "summary": "272 passed, 2 warnings (pre-existing)"
    },
    {
      "command": "cd frontend && npm run lint",
      "result": "passed",
      "summary": "0 errors, 0 warnings"
    },
    {
      "command": "cd frontend && npm run build",
      "result": "passed",
      "summary": "tsc + vite build clean"
    },
    {
      "command": "cd frontend && npm run test",
      "result": "passed",
      "summary": "36 files / 121 tests passed"
    }
  ],
  "validationOutput": [
    "Backend list_widgets supports service_id + scope filtering; test covers all/dash scope/service scope/filtered.",
    "Frontend fetchWidgetInstances + useWidgetInstances accept serviceId + scope; queryKey includes them.",
    "Dashboard uses scope=dashboard to exclude service-scoped widgets.",
    "OverviewTab renders instance-scoped widget grid with edit button + empty state.",
    "stubs.tsx deleted (all stubs replaced)."
  ],
  "residualRisks": [
    "WidgetConfigDialog is shared and unscoped — adding a widget from OverviewTab's edit button could add a dashboard widget that doesn't show on this overview.",
    "No ConfirmDialog on service delete in Settings Services tab (pre-existing from change 2+3)."
  ],
  "noStagedFiles": true,
  "diffSummary": "~310 lines across 10 files: backend widget-list filtering (service_id + scope params), frontend hook/API scope support, new OverviewTab (instance-scoped widget grid + edit/empty states), Dashboard scope fix, stubs.tsx deleted, ServicePage test mocks updated.",
  "reviewFindings": [
    "no blockers"
  ],
  "manualNotes": "Nothing is staged. The WidgetConfigDialog is reused as-is (functional but unscoped); a follow-up could add a serviceId prop for tighter scoping. The all_widgets kwarg on list_widgets is unused but kept for API clarity."
}