# 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 1. **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). 2. **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. 3. **GrafanaLinkCard's Button asChild + `` pattern produces a pi-lens advisory** ("nested `` tags"). This is the identical pattern from the original ObservabilityPage.tsx (shadcn `Button asChild` merges props into the child `` — it doesn't create a nested ``). Not a real issue; build and lint pass. 4. **LinksTab reads `instance.config.base_url`** for 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. ```acceptance-report { "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- advisory on LinksTab's GrafanaLinkCard is a false positive on the standard shadcn Button asChild + pattern (same as the original ObservabilityPage). Build and lint pass." }