8d2e4c9bfd
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.
7.3 KiB
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-2grid viaWidgetInstanceCard. - "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
- WidgetConfigDialog is unscoped. It lists all widget instances. The OverviewTab filters by
instance.idat render time, but the dialog shows everything. Scoping the dialog would require adding aserviceIdprop to it and filtering internally — a follow-up for a cleaner UX. - stubs.tsx deleted. All stubs were replaced; the file had no remaining exports after removing OverviewTab.
- ServicePage tests updated. Added mocks for
useWidgets,WidgetConfigDialog, andWidgetInstanceCardsince 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_widgetsparam onlist_widgetswas simplified to justservice_id+scope(theall_widgetskwarg 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."
}