Frontend: data-driven nav + service-page tab skeleton + stubs (Slice 4)

The IA shell lands. The static navItems array is replaced by useNavItems(),
which combines useServiceInstances (enabled instances) + useDashboards to
build the nav in spec order: Main Dashboard, named dashboards, conditional
service-type entries (one per configured type; ssh_tasks contributes Files
+ Actions, nextcloud contributes none), Services, Settings.

Legacy top-level routes (/media, /files, /actions, /users, /observability,
/backups, /monitoring, /applications) are removed; a NotFoundPage catch-all
returns 404 (R4.7).

ServicePage is refactored to a tab skeleton: Overview | type-specific
content tabs | Widgets | Config. serviceContentTabs(type) returns the
per-type set (jellyfin=Media+Requests, ssh_tasks=Files+Actions, backups=Jobs,
authentik=Users+Messaging, alertmanager=Alerts, grafana=Links,
prometheus=Metrics, nextcloud=none). Content tabs are stubs ('coming soon');
real content migrates in slices 5-9. Widgets + Config tabs preserve the
existing widget-list and config/secrets editing verbatim.

ServiceTypePage resolves /services/:type (no id) by redirecting to the
first enabled instance; empty state when none.

Instance switcher (Select) appears when >1 ENABLED sibling of the same
type exists (R3.1).

Empty states: Dashboard shows an 'Add a service' CTA when no instances
exist; ServicesPage already had a strong empty state.

Fixes from Slice 4 review:
- B1 (blocker): secret editing regressed because buildInput() hardcoded
  secrets:{} after the ConfigBody lift orphaned draftSecrets. Lifted
  draftSecrets to the parent ServicePage; buildInput now sends only the
  non-blank typed drafts ('leave blank to keep' semantics restored).
- S1: switcher trigger keys off enabled siblings, not total.

New: navEntries.ts + test, dashboards api/hook, service-tabs/ stubs +
index, ServiceTypePage, ServicePage tab skeleton + ConfigBody lift,
Dashboard empty-state CTA, ServicePage tab/switcher/secret-save tests.

Note: this branch is based on main (mobile-responsive-parity is unmerged);
the mobile SheetForm on ServicePage will be re-added when content tabs
get real content (slices 5-9). 84 tests pass (+1 secret-save guard);
lint/build green.

Refs openspec/changes/services-as-hub-ia/ (spec R1-R4/R9, tasks slice 4).
This commit is contained in:
Developer
2026-06-26 19:03:18 +00:00
parent a43d6a6206
commit caf6c226ff
12 changed files with 830 additions and 150 deletions
+69
View File
@@ -0,0 +1,69 @@
/**
* Service-page content tab stubs.
*
* Each stub renders a "coming soon" placeholder. Slices 59 replace these with
* real operational content lifted from the old top-level pages. All stubs accept
* an `instance` prop so the real implementations can scope queries by instance.
*/
import type { ServiceInstance } from "../../types";
import { Alert, AlertDescription } from "@/components/ui/alert";
function Stub({
label,
instance,
}: {
label: string;
instance: ServiceInstance;
}) {
return (
<Alert>
<AlertDescription>
{label} for {instance.name} coming soon.
</AlertDescription>
</Alert>
);
}
export function OverviewTab({ instance }: { instance: ServiceInstance }) {
return <Stub label="Service overview" instance={instance} />;
}
export function MediaTab({ instance }: { instance: ServiceInstance }) {
return <Stub label="Media" instance={instance} />;
}
export function RequestsTab({ instance }: { instance: ServiceInstance }) {
return <Stub label="Requests" instance={instance} />;
}
export function FilesTab({ instance }: { instance: ServiceInstance }) {
return <Stub label="Files" instance={instance} />;
}
export function ActionsTab({ instance }: { instance: ServiceInstance }) {
return <Stub label="Actions" instance={instance} />;
}
export function JobsTab({ instance }: { instance: ServiceInstance }) {
return <Stub label="Backup jobs" instance={instance} />;
}
export function UsersTab({ instance }: { instance: ServiceInstance }) {
return <Stub label="Users" instance={instance} />;
}
export function MessagingTab({ instance }: { instance: ServiceInstance }) {
return <Stub label="Messaging" instance={instance} />;
}
export function AlertsTab({ instance }: { instance: ServiceInstance }) {
return <Stub label="Alerts" instance={instance} />;
}
export function LinksTab({ instance }: { instance: ServiceInstance }) {
return <Stub label="Links" instance={instance} />;
}
export function MetricsTab({ instance }: { instance: ServiceInstance }) {
return <Stub label="Metrics" instance={instance} />;
}