From ff3df6749201cb150df3abd2493cdfad3073701c Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:12:45 +0100 Subject: [PATCH] Read platform alert Started as an age that keeps moving The Docker, Kubernetes, TrueNAS and vSphere alert tables showed Started as "Oct 6, 10:34 AM". It clipped on phones and at 768px. An open alert's Started cell answers how long it has been going on, so it now reads as an age ("2h ago"), as alert cards do. The exact time is the cell's hover text and stays in the drawer. Docker's column takes the numeric-value alignment the other three use. Making it an age exposed a wider bug: platform tables keep rows mounted across data refreshes, so an age computed at render froze while its timestamp did not change, reading "2h ago" three hours later. That already applied to every PlatformTableRelativeTimeValue cell (Kubernetes events, Docker volumes, vSphere activity and others). A shared clock, useRelativeTimeNow, now ticks every 30 seconds while any cell reads it and stops when none do. The relative-time cell and the alert tables pass it to formatRelativeTime as `now`, and the guardrails pin that. --- .../subsystems/frontend-primitives.md | 15 ++++ .../internal/subsystems/unified-resources.md | 3 +- .../a33fb3954747f9d2.json | 71 +++++++++++++++++++ .../SharedPrimitives.guardrails.test.ts | 7 ++ .../src/features/docker/DockerAlertsTable.tsx | 10 ++- .../kubernetes/KubernetesAlertsTable.tsx | 6 +- .../platformPage/sharedPlatformPage.tsx | 8 ++- .../features/truenas/TrueNASAlertsTable.tsx | 6 +- .../features/vmware/VsphereAlertsTable.tsx | 6 +- .../__tests__/VsphereAlertsTable.test.tsx | 41 ++++++++++- ...DetailPresentation.branchcov0724pm.test.ts | 27 +++---- .../utils/__tests__/relativeTimeClock.test.ts | 31 ++++++++ .../src/utils/alertDetailPresentation.ts | 14 ++-- frontend-modern/src/utils/format.ts | 6 +- .../src/utils/relativeTimeClock.ts | 32 +++++++++ 15 files changed, 246 insertions(+), 37 deletions(-) create mode 100644 frontend-modern/browser-verification/a33fb3954747f9d2.json create mode 100644 frontend-modern/src/utils/__tests__/relativeTimeClock.test.ts create mode 100644 frontend-modern/src/utils/relativeTimeClock.ts 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())} +