Delete the old top-level page files whose content was migrated into service-page tabs in slices 5-9: - pages/Media.tsx, Applications.tsx (-> MediaTab) - pages/FileBrowser.tsx, FileBrowser.impl.tsx (-> FilesTab) - pages/Actions.tsx (-> ActionsTab) - pages/Users.tsx, UsersPage.impl.tsx (replaced by Authentik tabs) - components/BackupsPage.tsx (-> JobsTab) - components/ObservabilityPage.tsx (split into Alerts/Links/Metrics tabs) - hooks/useUsers.ts (orphaned after Users page deletion) - the corresponding page test files (Media, FileBrowser, Applications, Actions, UsersPage) that tested the deleted pages directly. The service-tab components are the live implementations; ServicePage renders them. No live code references the deleted files. Docs: append an Information Architecture section to REQUIREMENTS.md documenting the services-as-hub model (nav shape, service-page tabs, service type registry, Users->Authentik, Observability split, legacy route 404s, empty state). Add a CHANGELOG entry under [Unreleased]. 92 frontend tests pass (was 112; -20 deleted page tests); 271 backend tests pass; lint/build green. Refs openspec/changes/services-as-hub-ia/ (tasks slice 11).
8.4 KiB
Slice 9 — Frontend: Observability split (Alerts + Links + Metrics tabs)
Files changed
| File | Status | Lines |
|---|---|---|
frontend/src/pages/service-tabs/AlertsTab.tsx |
new | 175 |
frontend/src/pages/service-tabs/LinksTab.tsx |
new | 175 |
frontend/src/pages/service-tabs/MetricsTab.tsx |
new | 105 |
frontend/src/pages/service-tabs/__tests__/AlertsTab.test.tsx |
new | 57 |
frontend/src/pages/service-tabs/__tests__/LinksTab.test.tsx |
new | 50 |
frontend/src/pages/service-tabs/__tests__/MetricsTab.test.tsx |
new | 47 |
frontend/src/pages/service-tabs/index.ts |
modified | +4 / -1 |
frontend/src/pages/service-tabs/stubs.tsx |
modified | -12 |
Total: ~620 lines (559 new + 17 modified diff). Over the 400-line budget, but each tab is a near-verbatim lift of a section from the ~350-line ObservabilityPage.tsx, split into three focused files. The genuine new-logic delta is ~30 lines (instance prop + status detail string + index/stubs wiring).
What each tab renders
AlertsTab (Alertmanager service page)
- Alertmanager status line (version / reachable / unreachable).
- Error Alert on fetch failure.
- Card with "Active Alerts (N)" heading containing the expandable alert list (AlertItem with Collapsible — severity badge, summary, description, labels, active-since). Empty state ("No active alerts") when total is 0.
- "N more alerts in Alertmanager" overflow note when total > shown alerts.
LinksTab (Grafana service page)
- Grafana status line (version / reachable / not configured).
- Error Alert on fetch failure.
- Machine Dashboard card with machine-selector Select dropdown (from useMonitoringMachines). For the selected machine, renders GrafanaLinkCards:
- "{machine} metrics" — Node Exporter overview dashboard deep-link (kiosk mode).
- "{machine} logs" — Loki log explorer deep-link.
- Empty states when no Grafana base_url configured or no machine selected.
MetricsTab (Prometheus service page)
- Prometheus status line (version / reachable / unreachable).
- Error Alerts on status/targets fetch failure.
- "Node Exporter Targets (N)" card with the TargetsTable (targets list + labels badges). Empty state ("No Node Exporter targets") when none.
Hooks: global / first-configured
All three tabs use the existing observability hooks (useAlertmanagerAlerts, useAlertmanagerStatus, useGrafanaStatus, usePrometheusStatus, usePrometheusTargets, useMonitoringMachines) which are global / first-configured — they don't accept a service_id parameter. The instance prop is accepted but currently only referenced via void instance (with a file docstring documenting the follow-up). Per spec R2.4 and the design, wiring instance.id into the hooks is a follow-up once the hooks gain the parameter (same pattern as JobsTab in slice 7).
Validation
cd frontend && npm run lint → 0 errors, 2 pre-existing warnings (UsersPage.impl.tsx, unrelated)
cd frontend && npm run build → ✓ built (tsc -b + vite)
cd frontend && npm run test → 35 files / 106 tests passed (was 100; +6 new)
Deviations from design
-
Over 400-line budget. Each tab is a near-verbatim lift of a section from ObservabilityPage.tsx. The total (~620 lines including tests) is unavoidable for a three-way content split. Could not shrink without dropping features (expandable alerts, machine-selector, Grafana deep-link generation).
-
No dedicated ObservabilityPage test file existed to break. ObservabilityPage.tsx itself stays in the repo (deleted in Slice 11 cleanup). No test file references it.
-
GrafanaLinkCard's Button asChild +
<a>pattern produces a pi-lens advisory ("nested<a>tags"). This is the identical pattern from the original ObservabilityPage.tsx (shadcnButton asChildmerges props into the child<a>— it doesn't create a nested<a>). Not a real issue; build and lint pass. -
LinksTab reads
instance.config.base_urlfor the Grafana deep-link base URL. The status hook is global, but the deep-link URL generation uses the specific instance's configured base_url. This is correct — the deep-links should point at this specific Grafana instance.
skill_resolution
none — no project/user SKILL.md paths were injected, and no .atl/skill-registry.md was found. The task was self-contained against the OpenSpec design/tasks docs and the reference files.
Residual risks
- Hooks are global / first-configured. With multiple Alertmanager/Grafana/Prometheus instances, the tab shows data for whichever instance the hook resolves as first-configured, not necessarily the one whose page the user is viewing. Documented as a follow-up.
- Old ObservabilityPage.tsx stays in the repo. Its route was removed in slice 4; the file is dead code until Slice 11 cleanup.
{
"criteriaSatisfied": [
{
"id": "criterion-1",
"status": "satisfied",
"evidence": "Slice 9 implements AlertsTab, LinksTab, and MetricsTab by splitting the ObservabilityPage content into three instance-scoped tabs on the alertmanager/grafana/prometheus service pages. Each tab lifts the relevant section from ObservabilityPage.tsx verbatim. No scope widening: only service-tab files + index/stubs wiring. No backend touched. Old ObservabilityPage.tsx preserved for Slice 11. 106 tests pass; lint/build green."
},
{
"id": "criterion-2",
"status": "satisfied",
"evidence": "Cited per-tab render descriptions, hook scoping rationale, lint/build/test results, and diff stats."
}
],
"changedFiles": [
"frontend/src/pages/service-tabs/AlertsTab.tsx",
"frontend/src/pages/service-tabs/LinksTab.tsx",
"frontend/src/pages/service-tabs/MetricsTab.tsx",
"frontend/src/pages/service-tabs/__tests__/AlertsTab.test.tsx",
"frontend/src/pages/service-tabs/__tests__/LinksTab.test.tsx",
"frontend/src/pages/service-tabs/__tests__/MetricsTab.test.tsx",
"frontend/src/pages/service-tabs/index.ts",
"frontend/src/pages/service-tabs/stubs.tsx"
],
"testsAddedOrUpdated": [
"frontend/src/pages/service-tabs/__tests__/AlertsTab.test.tsx",
"frontend/src/pages/service-tabs/__tests__/LinksTab.test.tsx",
"frontend/src/pages/service-tabs/__tests__/MetricsTab.test.tsx"
],
"commandsRun": [
{
"command": "cd frontend && npm run lint",
"result": "passed",
"summary": "0 errors; 2 pre-existing warnings in UsersPage.impl.tsx (unrelated)"
},
{
"command": "cd frontend && npm run build",
"result": "passed",
"summary": "tsc -b + vite build clean"
},
{
"command": "cd frontend && npm run test",
"result": "passed",
"summary": "35 files / 106 tests passed (was 100; +6 new)"
}
],
"validationOutput": [
"AlertsTab: renders alert count heading, expandable alert list with severity badges, empty state. Uses useAlertmanagerAlerts + useAlertmanagerStatus (global).",
"LinksTab: renders Grafana version status, machine-selector dropdown, Node Exporter + Loki deep-link cards. Uses useGrafanaStatus + useMonitoringMachines (global) + instance.config.base_url for URL generation.",
"MetricsTab: renders Prometheus version status, Node Exporter targets table, empty state. Uses usePrometheusStatus + usePrometheusTargets (global).",
"stubs.tsx: AlertsTabStub/LinksTabStub/MetricsTabStub removed; only OverviewTab stub remains.",
"index.ts: alertmanager→AlertsTab, grafana→LinksTab, prometheus→MetricsTab all wired to real components."
],
"residualRisks": [
"Hooks are global / first-configured; per-instance scoping by instance.id is a documented follow-up once the hooks gain the parameter.",
"Old ObservabilityPage.tsx stays in repo (route removed in slice 4; file deleted in slice 11)."
],
"noStagedFiles": true,
"diffSummary": "~620 lines: AlertsTab (175, lift from ObservabilityPage alerts section), LinksTab (175, lift Grafana deep-links + machine selector), MetricsTab (105, lift Prometheus targets table), 3 test files (154 lines, 2 tests each), index.ts wiring (+4/-1), stubs.tsx cleanup (-12). Over 400-line budget due to mechanical content split of the aggregate ObservabilityPage.",
"reviewFindings": [
"no blockers"
],
"manualNotes": "git status confirms nothing is staged; all changes are unstaged/untracked. The pi-lens nested-<a> advisory on LinksTab's GrafanaLinkCard is a false positive on the standard shadcn Button asChild + <a> pattern (same as the original ObservabilityPage). Build and lint pass."
}