From 29b91f22ce203716106c9225be23755658ee37fc Mon Sep 17 00:00:00 2001 From: Remylus Losius Date: Sat, 20 Jun 2026 16:14:43 -0400 Subject: [PATCH] feat(host-detail): live Activity tab (host-scoped feed) (Phase 2a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit frontend-host-detail v1.7.0 (AC-43). Phase 2a of the activity readability initiative (docs/engineering/activity_readability_plan.md). The host-detail Activity tab was a deferred stub, so the Recent-activity card's 'View all' link dead-ended. It now mounts HostActivityTab — the host-scoped unified feed at GET /api/v1/activity?host_id=X with cursor pagination (useInfiniteQuery + Load more), source filter chips (All/Monitoring/Compliance/Intelligence/Alert), and the shared ActivityRow + eventDisplay rendering. Activity joins Compliance/Remediation as a live tab (removed from the TAB_BACKEND_SUBSYSTEM stub registry), mirroring the exact precedent set when Compliance went live. The Audit log tab stays a stub for now: audit events carry no host_id, so a host-scoped audit view needs backend resource=host filtering (Phase 2b), which is also where the keep-vs-drop decision for that tab is made. Verified live: the owas-tst01 Activity tab renders the paginated feed with working filter chips. Full frontend suite (320) + specter (111) green. --- frontend/src/pages/HostDetailPage.tsx | 136 +++++++++++++++++- .../pages/host-detail-compliance-tab.test.tsx | 6 +- .../tests/pages/host-detail-shell.test.ts | 21 +++ specs/frontend/host-detail.spec.yaml | 8 +- 4 files changed, 164 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/HostDetailPage.tsx b/frontend/src/pages/HostDetailPage.tsx index b3a1e1ff..cab3796c 100644 --- a/frontend/src/pages/HostDetailPage.tsx +++ b/frontend/src/pages/HostDetailPage.tsx @@ -1,4 +1,4 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useParams, useSearch, useNavigate, Link } from '@tanstack/react-router'; import { useEffect, useMemo, useState, type CSSProperties, type ReactNode } from 'react'; import { @@ -182,7 +182,7 @@ const TAB_ORDER: { id: TabId; label: string; icon: LucideIcon }[] = [ // Backend subsystem that populates each tab when it lands. Surfaces // inside the per-tab empty state so operators know what's deferred. const TAB_BACKEND_SUBSYSTEM: Record< - Exclude, + Exclude, string > = { packages: 'Server Intelligence collection — installed-package inventory deferred (BACKLOG).', @@ -191,7 +191,6 @@ const TAB_BACKEND_SUBSYSTEM: Record< network: 'Server Intelligence collection — interfaces and firewall rules deferred (BACKLOG).', audit_log: 'Audit query API — host-scoped audit feed deferred to the unified /activity page (BACKLOG).', - activity: 'Unified Activity feed — combined transactions + audits + alerts deferred (BACKLOG).', terminal: 'Web terminal — SSH-in-browser deferred; use a host-side SSH client in the meantime.', }; @@ -487,6 +486,8 @@ export function HostDetailPage() { /> ) : activeTab === 'remediation' ? ( + ) : activeTab === 'activity' ? ( + ) : ( )} @@ -2545,6 +2546,135 @@ function ActivityRow({ item }: { item: ActivityItem }) { ); } +// Source filter chips for the host Activity tab. Audit is omitted: audit +// events carry no host_id, so a host-scoped audit filter is empty by design +// (host-relevant audit by resource is a Phase 2b follow-up). The unified +// feed otherwise covers monitoring, compliance, intelligence, and alerts. +const HOST_ACTIVITY_SOURCES: { id: ActivitySource | ''; label: string }[] = [ + { id: '', label: 'All' }, + { id: 'monitoring', label: 'Monitoring' }, + { id: 'transaction', label: 'Compliance' }, + { id: 'intelligence', label: 'Intelligence' }, + { id: 'alert', label: 'Alert' }, +]; + +// HostActivityTab is the full host-scoped activity feed (the "View all" +// target from the Recent activity card). It pages the unified +// /api/v1/activity?host_id=X endpoint (cursor pagination) and reuses +// ActivityRow + the shared eventDisplay helpers. Spec frontend-host-detail. +function HostActivityTab({ hostId }: { hostId: string }) { + const [source, setSource] = useState(''); + const q = useInfiniteQuery({ + queryKey: ['host', hostId, 'activity', source], + initialPageParam: undefined as string | undefined, + queryFn: async ({ pageParam }) => { + const { data, error, response } = await api.GET('/api/v1/activity', { + params: { + query: { + host_id: hostId, + limit: 50, + ...(source ? { source } : {}), + ...(pageParam ? { cursor: pageParam } : {}), + }, + }, + }); + if (error || !response.ok) { + throw new Error(apiErrorMessage(error, `Failed to load activity (${response.status})`)); + } + return data as unknown as { items: ActivityItem[]; next_cursor?: string | null }; + }, + getNextPageParam: (last) => last.next_cursor ?? undefined, + enabled: !!hostId, + }); + + const items = q.data?.pages.flatMap((p) => p.items ?? []) ?? []; + + return ( + +
+ {HOST_ACTIVITY_SOURCES.map((opt) => { + const active = source === opt.id; + return ( + + ); + })} +
+ + {q.isLoading ? ( +
Loading…
+ ) : q.isError ? ( +
+ {apiErrorMessage(q.error, 'Failed to load activity')}{' '} + +
+ ) : items.length === 0 ? ( + + ) : ( + <> +
    + {items.map((it) => ( + + ))} +
+ {q.hasNextPage ? ( + + ) : null} + + )} +
+ ); +} + // activitySeverityColors maps the closed severity enum onto the // existing OW color tokens. function activitySeverityColors(s: ActivitySeverity): { fg: string; dot: string } { diff --git a/frontend/tests/pages/host-detail-compliance-tab.test.tsx b/frontend/tests/pages/host-detail-compliance-tab.test.tsx index adb38d85..8def2862 100644 --- a/frontend/tests/pages/host-detail-compliance-tab.test.tsx +++ b/frontend/tests/pages/host-detail-compliance-tab.test.tsx @@ -156,8 +156,10 @@ describe('frontend-host-compliance-tab — structural', () => { expect(PAGE_SRC).toContain('"); + // remediation + activity joined overview + compliance as live (non-stub) tabs. + expect(PAGE_SRC).toContain( + "Exclude", + ); }); // @ac AC-02 diff --git a/frontend/tests/pages/host-detail-shell.test.ts b/frontend/tests/pages/host-detail-shell.test.ts index d2578872..6090701b 100644 --- a/frontend/tests/pages/host-detail-shell.test.ts +++ b/frontend/tests/pages/host-detail-shell.test.ts @@ -481,4 +481,25 @@ describe('frontend-host-detail v1.6.0 — per-host credential management + recon // Mutating controls gated on credential:write. expect(HOST_CRED_SRC).toContain("hasPermission('credential:write')"); }); + + // @ac AC-43 + test('frontend-host-detail/AC-43 — Activity tab is live (HostActivityTab), not a stub', () => { + // Activity is removed from the deferred-stub registry and routed to the + // host-scoped feed component. + expect(PAGE_SRC).toContain('function HostActivityTab'); + expect(PAGE_SRC).toMatch(/activeTab === 'activity' \? \(\s*/); + // Paged host-scoped feed via useInfiniteQuery with cursor pagination. + expect(PAGE_SRC).toContain('useInfiniteQuery'); + expect(PAGE_SRC).toMatch(/getNextPageParam:\s*\(last\)\s*=>\s*last\.next_cursor/); + expect(PAGE_SRC).toMatch(/host_id:\s*hostId/); + // Source filter chips + Load more + reuse of ActivityRow. + expect(PAGE_SRC).toContain('HOST_ACTIVITY_SOURCES'); + expect(PAGE_SRC).toMatch(/hasNextPage/); + expect(PAGE_SRC).toMatch(/Load more/); + // The Audit log tab remains a stub. + expect(PAGE_SRC).toMatch(/audit_log:\s*\n?\s*'Audit query API/); + }); }); diff --git a/specs/frontend/host-detail.spec.yaml b/specs/frontend/host-detail.spec.yaml index 8a65f999..5454b65c 100644 --- a/specs/frontend/host-detail.spec.yaml +++ b/specs/frontend/host-detail.spec.yaml @@ -1,7 +1,7 @@ spec: id: frontend-host-detail title: Host Detail page layout, behavior, and empty-state contracts - version: "1.6.0" + version: "1.7.0" status: approved tier: 2 @@ -70,7 +70,7 @@ spec: type: technical enforcement: error - id: C-04 - description: 'Tabs row MUST render all 10 prototype tabs in order: Overview, Compliance, Packages, Services, Users, Network, Audit log, Activity, Remediation, Terminal. Tabs whose backend subsystem is still deferred render an empty-state stub that NAMES the subsystem. v1.3.0: Compliance is live (frontend-host-compliance-tab) and the TAB_BACKEND_SUBSYSTEM stub registry no longer carries a compliance entry; Packages / Services / Users / Network are live per frontend-host-detail-inventory-tabs' + description: 'Tabs row MUST render all 10 prototype tabs in order: Overview, Compliance, Packages, Services, Users, Network, Audit log, Activity, Remediation, Terminal. Tabs whose backend subsystem is still deferred render an empty-state stub that NAMES the subsystem. v1.3.0: Compliance is live (frontend-host-compliance-tab) and the TAB_BACKEND_SUBSYSTEM stub registry no longer carries a compliance entry; Packages / Services / Users / Network are live per frontend-host-detail-inventory-tabs. v1.7.0: Activity is live (mounts HostActivityTab, the host-scoped /api/v1/activity feed) and is likewise removed from the TAB_BACKEND_SUBSYSTEM stub registry. The Audit log tab remains a stub (host-scoped audit by resource is a follow-up)' type: technical enforcement: error - id: C-05 @@ -254,3 +254,7 @@ spec: description: 'v1.6.0 - the Connectivity card "Edit credentials" button and the EditHostModal "Manage SSH credential" link both open HostCredentialModal for the host. HostCredentialModal resolves the source and renders the four tier transitions (clone default, set different host credential, edit override via PATCH, revert override via DELETE), each invalidating ["host-credential-resolve", hostId]; mutating controls are gated on credential:write. Source-inspection of HostDetailPage.tsx, EditHostModal.tsx, and HostCredentialModal.tsx.' priority: high references_constraints: [C-11, C-12] + - id: AC-43 + description: 'v1.7.0 source-inspection - the Activity tab mounts HostActivityTab (not TabStub): it is removed from the TAB_BACKEND_SUBSYSTEM stub registry, and the tab render switch routes activeTab === "activity" to . HostActivityTab pages GET /api/v1/activity?host_id=X via useInfiniteQuery (cursor pagination, getNextPageParam from next_cursor), renders rows with the shared ActivityRow, exposes source filter chips (All / Monitoring / Compliance / Intelligence / Alert) that drive the source query param, shows a Load more control gated on hasNextPage, and renders loading / error+Retry / "No activity yet" empty states. The Audit log tab still renders TabStub.' + priority: high + references_constraints: [C-04]