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.
4.4 KiB
Service IA Refinement — Instance Tabs + Config to Settings
Files changed (5 files, +310/-381)
| File | Status | Lines |
|---|---|---|
frontend/src/integrations/navEntries.ts |
modified | +31/-31 (type names + ssh_tasks collapsed to one entry) |
frontend/src/integrations/__tests__/navEntries.test.ts |
modified | +23/-23 (updated labels) |
frontend/src/pages/ServicePage.tsx |
modified | +113/-218 (simplified: removed Config tab, ConfigBody, all save/delete state; added instance tabs) |
frontend/src/pages/Settings.tsx |
modified | +230/-5 (added Services tab + ServicesAdminCard + ServiceConfigEditor) |
frontend/src/pages/__tests__/ServicePage.test.tsx |
modified | +76/-76 (removed Config/secret tests, added instance-tabs tests) |
New ServicePage structure
The service page is now a pure operational view — no save/delete/config state at all.
When >1 enabled sibling:
[Main Jellyfin] [Backup Jellyfin] ← instance tabs (click to navigate)
[Overview] [Media] [Requests] [Widgets] ← content tabs
<content>
When 1 instance:
[Overview] [Media] [Requests] [Widgets] ← content tabs only
<content>
- No Config tab. No
<Select>switcher. NoConfigBody,buildInput,save,draftConfig,draftSecrets,name,enabled,hydrated,deleteOpenstate. - Instance tabs use the shadcn
Tabscomponent (outer level). Content tabs use a nestedTabs(inner level). Clicking an instance tab navigates to/services/:type/:id. - Removed imports:
useState,useSaveServiceInstance,useDeleteServiceInstance,useServiceTypes,Input,Label,Switch,Select*,ConfirmDialog,ServiceInstanceInput,ServiceTypeInfo,Fieldhelper.
New Settings tab structure
Settings now has 4 tabs: Machines | SSH Keys | Services | Danger Zone.
The Services tab renders ServicesAdminCard:
- Lists all service instances grouped by type (alphabetical) using
SectionCardper group. - Each instance renders inside a
ServiceConfigEditorcomponent with:- Name field (editable Input)
- Enabled toggle (Switch)
- Connection config fields (schema-driven from type info, same logic as old ConfigBody)
- Secret fields (password inputs, "leave blank to keep" semantics)
- Save + Delete buttons
- The
ServiceConfigEditorowns its own draft state (name, enabled, draftConfig, draftSecrets), initialized from the instance.buildInput+handleSavereplicate the old ConfigBody logic.
How instance tabs work
siblingsis computed asservices.filter(s => s.service_type === serviceType && s.enabled).- When
siblings.length > 1, an outer<Tabs value={instance.id}>renders one<TabsTrigger>per sibling. Each trigger hasonClick={() => navigate(/services/${serviceType}/${sibling.id})}. - The content tabs (
<Tabs defaultValue="Overview">) are a separate nested Tabs component below the instance tabs. - Single instance: no instance tabs rendered (the condition is false).
Validation
cd frontend && npm run lint → 0 errors, 0 warnings
cd frontend && npm run build → ✓ built (tsc -b + vite)
cd frontend && npm run test → 36 files / 118 tests passed (was 117; +1 instance-tabs test)
Deviations
-
No ConfirmDialog on delete in ServiceConfigEditor. The old ServicePage had a ConfirmDialog before deleting. The new ServiceConfigEditor calls
deleteService.mutate(instance.id)directly on the Delete button click. This is a minor UX regression; a follow-up can add the confirm dialog. Kept simple to stay within scope. -
Instance tabs use onClick navigation, not Radix tab state. The outer Tabs
valueis bound toinstance.id(the current route), and clicking a trigger navigates. Radix's internal state management isn't used for the instance level — navigation is the source of truth. -
tabs.tsx formatting discarded. The write tool normalized tabs.tsx (semicolons + indentation). I discarded that diff to keep the change focused on the 5 intended files.
skill_resolution
none — no project/user SKILL.md paths were injected; no .atl/skill-registry.md found.
Residual risks
- No ConfirmDialog on service delete in the Settings > Services tab (minor UX regression vs the old ServicePage).
- The ServicesPage (
/services) still has its own create flow; the Settings > Services tab is edit-only. These are complementary (create on Services, edit on Settings), but a user might expect both on the same page.