diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 308d1470e2..4772ac4a62 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -5175,6 +5175,21 @@ Docker, Kubernetes, TrueNAS, and vSphere alert tables. Those tables must call `formatPlatformAlertEntityType`, `formatPlatformAlertStartedAt`, and `formatPlatformAlertDetailDateTime` instead of declaring local formatter helpers. +`formatPlatformAlertStartedAt` reads as an age ("2h ago"), because an open +alert's Started cell answers how long it has been going on, as alert cards do; +the absolute "Oct 6, 10:34 AM" also clipped at phone and tablet widths. Each +table's Started cell carries `formatPlatformAlertDetailDateTime` as its hover +title, the drawer's Started row keeps that exact time, an unset (pre-2000) or +invalid time stays "-", and the column uses the `numeric-value` alignment kind +on all four tables. +Relative ages read one shared clock. `useRelativeTimeNow` +(`frontend-modern/src/utils/relativeTimeClock.ts`) ticks every 30 seconds while +any mounted cell reads it and stops when none do; `PlatformTableRelativeTimeValue` +and the platform alert tables pass it as `now` to `formatRelativeTime` / +`formatPlatformAlertStartedAt`. Platform tables keep their rows mounted across +data refreshes, so an age computed only at render froze ("2h ago" three hours +later) whenever its timestamp did not change. A relative-age cell must read the +shared clock rather than `Date.now()` at render or a table-local interval. Read-only metadata badges follow the same primitive-owned shell rule. `frontend-modern/src/components/shared/MetadataBadge.tsx` owns filled and outlined appearances, compact sizing, shape, typed tone vocabulary, fit diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index a22df85cf1..3804b3df91 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -4293,7 +4293,8 @@ and platform alert detail fields must use `formatPlatformAlertDetailDateTime` from `frontend-modern/src/utils/alertDetailPresentation.ts` instead of restoring table-local provider code, resource-type, entity-type, or timestamp formatter -helpers, +helpers (the Started cell reads as an age with the exact time as its hover +title, the drawer keeps the exact time), and local operator identity labels now split from governed detail summaries: infrastructure tables, selectors, links, and drawer headings must preserve the canonical local instance identity (`displayName`, canonical display name, diff --git a/frontend-modern/browser-verification/a33fb3954747f9d2.json b/frontend-modern/browser-verification/a33fb3954747f9d2.json new file mode 100644 index 0000000000..dcad5c670f --- /dev/null +++ b/frontend-modern/browser-verification/a33fb3954747f9d2.json @@ -0,0 +1,71 @@ +{ + "version": 1, + "base_sha": "13d604e1c71a4b8388ecaf04ea4dbdcd731f7e0b", + "verified_at": "2026-10-06T13:10:03Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/features/docker/DockerAlertsTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx", + "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx", + "frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx", + "frontend-modern/src/features/vmware/VsphereAlertsTable.tsx", + "frontend-modern/src/utils/alertDetailPresentation.ts", + "frontend-modern/src/utils/format.ts", + "frontend-modern/src/utils/relativeTimeClock.ts" + ], + "content_sha256": { + "frontend-modern/src/features/docker/DockerAlertsTable.tsx": "752554fff4c5091823aed3b5f12434fa45a8c0ca43d5ad4367280dea938c8b4d", + "frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx": "7af91f77431771b4691a7b4b7e1ebd92656c38dc5aa4ecc72cd0e40f47df7664", + "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "6315591667690bdf7b588524b5c2506643a3a24d2269b8cf050d83e1e274fa97", + "frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx": "7361527eedafc6a3e5ef9fbea8689fd802c4bcc487f4741e3c5a10649ea64d03", + "frontend-modern/src/features/vmware/VsphereAlertsTable.tsx": "e8154ec75bcbb6daa6f3bf32fd8bc0749d6649bae8e31ac7e3c7be345feebcae", + "frontend-modern/src/utils/alertDetailPresentation.ts": "5c3e6beb5df3d255c01b38e566163e630dd93ed1eebb82539c68110c3bf027ea", + "frontend-modern/src/utils/format.ts": "7757da44a87231405223b7a88bbf820ed905b4823f7f2955b9c9f7f488672a57", + "frontend-modern/src/utils/relativeTimeClock.ts": "877686e957292e07dc233be81b52d97bccfa1fb8aeddc63a82b0abf4d5c5e41f" + }, + "routes": [ + "/vmware/overview", + "/vmware/activity", + "/truenas/overview", + "/docker/overview", + "/kubernetes/overview" + ], + "viewports": [ + { + "width": 390, + "height": 844 + }, + { + "width": 600, + "height": 844 + }, + { + "width": 768, + "height": 1000 + }, + { + "width": 1024, + "height": 1000 + }, + { + "width": 1440, + "height": 1000 + }, + { + "width": 1920, + "height": 1000 + } + ], + "states": [ + "Private mock stack on main 92d946d057 plus this change (rebased onto main 13d604e1c7, whose node-temperature alert and release-preflight changes touch no frontend file here)", + "Before: alert-table Started read 'Oct 6, 10:34 AM' and clipped on 2-3 rows at 390 and 768 on TrueNAS and vSphere (by 13-31px)", + "After: Started reads as an age ('9m ago', '2h ago') and clips at no width from 390 to 1920 on TrueNAS and vSphere; the cell's hover title is the exact time ('Oct 6, 2026, 11:52 AM') and the drawer's Started row keeps it", + "Docker and Kubernetes overviews have no provider incidents in the mock, so an incident started two hours earlier was injected into the API responses (websocket held): both read '2h ago' with the exact time on hover, right-aligned, unclipped at 390, 768 and 1440; Docker's Started column now uses the numeric-value alignment kind like the other three", + "Shared clock: with Playwright's page clock installed and the websocket held so no data changed, vSphere Started ages went from '9m ago' to '3h ago' after a 3-hour fast-forward and vSphere Activity from '1d ago' to '2d ago' after 24 hours; before this change the ages stayed frozen (unit test fails without the clock)" + ], + "interactions": [ + "Measured the Started column and every other column for clipping at six widths", + "Read the Started cell's hover title", + "Fast-forwarded the page clock with live data held and re-read the ages" + ] +} diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 7973df8bb9..527c07f433 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -2556,6 +2556,10 @@ describe('shared primitive guardrails', () => { expect(source).toContain('formatPlatformAlertResourceType'); expect(source).toContain('formatPlatformAlertStartedAt'); expect(source).toContain('formatPlatformAlertDetailDateTime'); + // Started reads as an age on the shared clock, the exact time on hover. + expect(source).toContain('useRelativeTimeNow'); + expect(source).toContain('formatPlatformAlertStartedAt(incident.startedAt, now())'); + expect(source).toContain('title={formatPlatformAlertDetailDateTime(incident.startedAt)}'); expect(source).not.toContain('severityVariant'); expect(source).not.toContain('severityTextClass'); expect(source).not.toContain('const alertTone'); @@ -6448,6 +6452,9 @@ describe('shared primitive guardrails', () => { expect(sharedPlatformPageSource).toContain('export function PlatformTableRelativeTimeValue'); expect(sharedPlatformPageSource).toContain('formatPlatformTableRelativeTimeValue'); + // Ages read the shared clock: rows stay mounted across refreshes, so an age + // computed only at render froze while its timestamp did not change. + expect(sharedPlatformPageSource).toContain('useRelativeTimeNow'); expect(sharedPlatformPageSource).toContain('formatRelativeTime(value'); for (const [path, source] of platformRelativeTimeConsumers) { diff --git a/frontend-modern/src/features/docker/DockerAlertsTable.tsx b/frontend-modern/src/features/docker/DockerAlertsTable.tsx index 95168b2761..295ab82f03 100644 --- a/frontend-modern/src/features/docker/DockerAlertsTable.tsx +++ b/frontend-modern/src/features/docker/DockerAlertsTable.tsx @@ -31,6 +31,7 @@ import { formatPlatformAlertResourceType, formatPlatformAlertStartedAt, } from '@/utils/alertDetailPresentation'; +import { useRelativeTimeNow } from '@/utils/relativeTimeClock'; import { getAlertFilteredEmptyState } from '@/utils/alertOverviewPresentation'; import { formatAlertSeverityLabel, @@ -112,6 +113,7 @@ export const DockerAlertsTable: Component<{ emptyDescription: string; showToolbar?: boolean; }> = (props) => { + const now = useRelativeTimeNow(); const tableState = createPlatformTableFilterState({ resources: () => props.incidents, initialStatus: 'all' as DockerIncidentSeverityFilter, @@ -186,7 +188,7 @@ export const DockerAlertsTable: Component<{ Host Started @@ -276,9 +278,11 @@ export const DockerAlertsTable: Component<{ - {formatPlatformAlertStartedAt(incident.startedAt)} + + {formatPlatformAlertStartedAt(incident.startedAt, now())} +