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",
]);
});
});
+13 -18
View File
@@ -1,19 +1,19 @@
/**
* Service-type → conditional nav-entry map.
*
* Each configured service type contributes one or more top-level nav entries
* that appear only when at least one enabled instance of that type exists.
* See OpenSpec change `services-as-hub-ia`, spec R1.2.
* Each configured service type contributes ONE top-level nav entry that
* appears only when at least one enabled instance of that type exists. The
* label is the service TYPE name (Jellyfin, SSH Tasks), not a conceptual
* name (Media, Files) — the service page's content tabs surface the concepts.
*/
import {
Activity,
DatabaseBackup,
FolderOpen,
GanttChartSquare,
Link2,
Monitor,
Server,
Users,
Zap,
type LucideIcon,
} from "lucide-react";
@@ -26,31 +26,26 @@ export interface NavEntry {
}
/**
* Static mapping from service type to its conditional nav entries.
* `nextcloud` has no entries (no operational content).
* Static mapping from service type to its conditional nav entry.
* Uses the service type's display name. One entry per type.
* `nextcloud` has no entry (no operational content tabs).
*/
export const SERVICE_TYPE_NAV_ENTRIES: NavEntry[] = [
{
serviceType: "jellyfin",
label: "Media",
label: "Jellyfin",
icon: Monitor,
path: "/services/jellyfin",
},
{
serviceType: "ssh_tasks",
label: "Files",
icon: FolderOpen,
path: "/services/ssh_tasks",
},
{
serviceType: "ssh_tasks",
label: "Actions",
icon: Zap,
label: "SSH Tasks",
icon: Server,
path: "/services/ssh_tasks",
},
{
serviceType: "alertmanager",
label: "Alerts",
label: "Alertmanager",
icon: Activity,
path: "/services/alertmanager",
},
@@ -74,7 +69,7 @@ export const SERVICE_TYPE_NAV_ENTRIES: NavEntry[] = [
},
{
serviceType: "authentik",
label: "Users",
label: "Authentik",
icon: Users,
path: "/services/authentik",
},