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.
This commit is contained in:
Developer
2026-06-26 22:25:46 +00:00
parent fef0ded76f
commit 8d2e4c9bfd
17 changed files with 812 additions and 422 deletions
@@ -6,17 +6,17 @@ describe("navEntries", () => {
expect(configuredNavEntries(new Set())).toEqual([]);
});
it("returns Media when jellyfin is configured", () => {
it("returns Jellyfin when jellyfin is configured", () => {
const entries = configuredNavEntries(new Set(["jellyfin"]));
expect(entries).toHaveLength(1);
expect(entries[0].label).toBe("Media");
expect(entries[0].label).toBe("Jellyfin");
expect(entries[0].path).toBe("/services/jellyfin");
});
it("returns Files + Actions when ssh_tasks is configured", () => {
it("returns one SSH Tasks entry when ssh_tasks is configured", () => {
const entries = configuredNavEntries(new Set(["ssh_tasks"]));
expect(entries).toHaveLength(2);
expect(entries.map((e) => e.label)).toEqual(["Files", "Actions"]);
expect(entries).toHaveLength(1);
expect(entries[0].label).toBe("SSH Tasks");
});
it("returns all observability entries", () => {
@@ -24,15 +24,15 @@ describe("navEntries", () => {
new Set(["alertmanager", "grafana", "prometheus"]),
);
expect(entries.map((e) => e.label)).toEqual([
"Alerts",
"Alertmanager",
"Grafana",
"Prometheus",
]);
});
it("returns Backups + Users when configured", () => {
it("returns Backups + Authentik when configured", () => {
const entries = configuredNavEntries(new Set(["backups", "authentik"]));
expect(entries.map((e) => e.label)).toEqual(["Backups", "Users"]);
expect(entries.map((e) => e.label)).toEqual(["Backups", "Authentik"]);
});
it("nextcloud has no nav entries in the static map", () => {
@@ -46,10 +46,9 @@ describe("navEntries", () => {
new Set(["authentik", "ssh_tasks", "jellyfin"]),
);
expect(entries.map((e) => e.label)).toEqual([
"Media",
"Files",
"Actions",
"Users",
"Jellyfin",
"SSH Tasks",
"Authentik",
]);
});
});