Files
manage/.pi-tmp/svc-hub-slice9-out.md
T
Developer a8dfbd5dc6 Cleanup: delete dead top-level pages + update docs (Slice 11)
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).
2026-06-26 20:11:02 +00:00

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

  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 + <a> pattern produces a pi-lens advisory ("nested <a> tags"). This is the identical pattern from the original ObservabilityPage.tsx (shadcn Button asChild merges props into the child <a> — it doesn't create a nested <a>). 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.
{
  "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."
}