# 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."
}