From 90f819b309ba110cbaddf09fb55b38a67e1cb515 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:59:11 +0100 Subject: [PATCH] Show Alert History as cards when its card is too narrow Alerts > History chose between its table and its phone cards by the viewport: the table from 768px. With the Alerts rail open, a 1024px to roughly 1240px window gives the table a 600px to 800px card, where its own CSS stacks the three row actions and resource names and messages break mid-word ("inventory-/api") or end after two lines. The table section now measures its card with useObservedElementWidth and shows the cards whenever the card is narrower than 800px, the width from which the row actions share one line; before the card is measured, the phone breakpoint decides as before. The cards take that decision as an `active` signal instead of their own 768px media query, so an open timeline or resource view still moves into the cards' drawer when they take over and back to the inline expansion when the table returns. The decision is a memo so a resize within one layout does not re-run that move and reopen a drawer the operator closed. --- .../v6/internal/subsystems/alerts.md | 15 ++++ .../subsystems/frontend-primitives.md | 8 ++ .../v6/internal/subsystems/registry.json | 2 + .../04d6d08630ddbb48.json | 45 +++++++++++ .../alerts/AlertHistoryMobileList.tsx | 36 +++++---- .../alerts/AlertHistoryTableSection.tsx | 30 +++++++- .../__tests__/AlertHistoryMobileList.test.tsx | 75 ++++++++++++------- .../AlertHistoryTableSection.test.tsx | 75 +++++++++++++++++++ .../canonical_completion_guard_test.py | 2 + 9 files changed, 246 insertions(+), 42 deletions(-) create mode 100644 frontend-modern/browser-verification/04d6d08630ddbb48.json create mode 100644 frontend-modern/src/features/alerts/__tests__/AlertHistoryTableSection.test.tsx diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index e28d3a39f0..04f7ac7add 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -1635,6 +1635,21 @@ locale. Cards narrower than 600px, which a window of about 900px with the rail open produces, are still cramped; a container-driven switch to the card list is the remaining fix. +The history card's own width now chooses between the table and the cards, +not the viewport. `AlertHistoryTableSection` measures its wrapper with +`useObservedElementWidth` and shows the cards whenever the card is narrower +than `ALERT_HISTORY_TABLE_MIN_WIDTH` (800px, the width from which the three +row actions share one line); until the card is measured, the phone +breakpoint decides as before. A 1024px to roughly 1240px window with the +Alerts rail open, which gave the table a 600px to 800px card with stacked +actions and names broken mid-word, now shows the cards; a 1280px window +(an 837px card) keeps the table. The 600px density and the stacked-action +rule above therefore serve only a card that has not been measured. The cards +take `active` from that decision instead of a 768px media query, so an open +timeline or resource view moves into the cards' drawer when the cards take +over and back to the inline expansion when the table returns +(`AlertHistoryTableSection.test.tsx`, `AlertHistoryMobileList.test.tsx`). + Alert history row timestamps render clock time in the viewer's own locale and must carry the absolute date and time as a title. The date otherwise lives only in the day group header, which scrolls out of sight, and a hardcoded diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index a85645139e..cdbbac05de 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -8933,6 +8933,14 @@ placeholders for the row subject (`alerts.assistant.iconLabel`, `alerts.assistant.iconLabelAt`); every locale keeps those placeholders (`i18n.test.ts`). +### Layouts that swap a table for cards follow the container + +A surface that swaps a table for a card list decides from the width of the +element that holds them (`useObservedElementWidth`), with the viewport +breakpoint only as the fallback before the first measurement, because side +rails and panels leave a table far less room than the screen width suggests. +Alerts > History switches below an 800px card (`ALERT_HISTORY_TABLE_MIN_WIDTH`). + ### Mobile destinations are links and update evidence stays explicit Fixed mobile destinations and menu destinations that navigate are anchors with diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 75b7ff1f62..59cf394727 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -2519,7 +2519,9 @@ "frontend-modern/src/components/Alerts/WebhookConfig.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertDeadManDestinationSection.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertHistoryFrequencyCard.test.tsx", + "frontend-modern/src/features/alerts/__tests__/AlertHistoryMobileList.test.tsx", "frontend-modern/src/features/alerts/__tests__/alertHistoryModel.test.ts", + "frontend-modern/src/features/alerts/__tests__/AlertHistoryTableSection.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertIncidentSynthesisSummary.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertIntentPolicyPanel.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertOverviewAlertCard.system-scope.test.tsx", diff --git a/frontend-modern/browser-verification/04d6d08630ddbb48.json b/frontend-modern/browser-verification/04d6d08630ddbb48.json new file mode 100644 index 0000000000..ff4ec31b0d --- /dev/null +++ b/frontend-modern/browser-verification/04d6d08630ddbb48.json @@ -0,0 +1,45 @@ +{ + "version": 1, + "base_sha": "fbf5b0c8beb89ce114b468cceb7f606255f9b33d", + "verified_at": "2026-10-15T13:00:00Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx", + "frontend-modern/src/features/alerts/AlertHistoryTableSection.tsx" + ], + "content_sha256": { + "frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx": "e2bfcc34a8a5bb93c38c1cda0e3d7da67a5593d28f89730b9356f91e92cedb4a", + "frontend-modern/src/features/alerts/AlertHistoryTableSection.tsx": "318f48211cf5fd1437299fa194e3a23becc64a788da143390f7b7cd473b15a5b" + }, + "routes": [ + "/alerts/history" + ], + "viewports": [ + { + "width": 1280, + "height": 800 + }, + { + "width": 1024, + "height": 768 + }, + { + "width": 900, + "height": 768 + }, + { + "width": 375, + "height": 812 + } + ], + "states": [ + "Before (main): a 1024px window with the Alerts rail open showed the table in a 602px card, names broken mid-word (\"inventory-/api\"), messages cut after two lines and the three row actions stacked", + "After: 1280px window, 837px card, table unchanged; 1024px, 602px card, cards with full names and messages; 900px (rail collapses to tabs), 774px card, cards; 375px phone, cards, no horizontal overflow", + "A timeline opened inline in the table at 1280px reappears as the card drawer at 1024px and returns to the inline expansion at 1280px", + "A card timeline opens the shared drawer at 1024px" + ], + "interactions": [ + "Resized between 1280, 1024, 900 and 375 and read the layout, card width and visibility of each layout", + "Opened a table timeline, narrowed to cards, widened back; opened and closed a card timeline drawer" + ] +} diff --git a/frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx b/frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx index beee22a902..28c0bb4f57 100644 --- a/frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx +++ b/frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx @@ -1,4 +1,4 @@ -import { Show, createMemo, createSignal, onCleanup, onMount } from 'solid-js'; +import { Show, createEffect, createMemo, createSignal, on, type Accessor } from 'solid-js'; import { aiChatStore } from '@/stores/aiChat'; import { useRelativeTimeNow } from '@/utils/relativeTimeClock'; @@ -20,6 +20,11 @@ import type { AlertHistoryState } from './useAlertHistoryState'; interface AlertHistoryMobileListProps { state: AlertHistoryState; + /** + * Whether the cards are the history layout on show. The table section + * decides from its card's width; a list rendered on its own is active. + */ + active?: Accessor; } type AlertHistoryGroup = ReturnType[number]; @@ -47,13 +52,16 @@ export function AlertHistoryMobileList(props: AlertHistoryMobileListProps) { return items; }); + // Only a change of layout may move expansions into the drawer: an accessor + // that reads a width would otherwise rerun this on every resized pixel and + // reopen a drawer the operator just closed. + const active = createMemo(() => props.active?.() ?? true); + // The shared expansion state survives layout changes. Project it into the - // phone's single drawer instead of leaving a hidden desktop expansion open. - onMount(() => { - if (typeof window.matchMedia !== 'function') return; - const viewport = window.matchMedia('(min-width: 768px)'); - const synchronizeLayout = () => { - if (viewport.matches || aiChatStore.isOpen) { + // cards' single drawer instead of leaving a hidden table expansion open. + createEffect( + on(active, (isActive) => { + if (!isActive || aiChatStore.isOpen) { setInvestigation(null); return; } @@ -76,11 +84,8 @@ export function AlertHistoryMobileList(props: AlertHistoryMobileListProps) { } } setInvestigation(null); - }; - synchronizeLayout(); - viewport.addEventListener('change', synchronizeLayout); - onCleanup(() => viewport.removeEventListener('change', synchronizeLayout)); - }); + }), + ); const getInvestigationReturnFocusTarget = ( current: MobileAlertHistoryInvestigation, @@ -177,7 +182,12 @@ export function AlertHistoryMobileList(props: AlertHistoryMobileListProps) { return ( <> -
+
[number]; type AlertHistoryAlert = AlertHistoryGroup['alerts'][number]; type AlertHistoryRenderItem = @@ -26,6 +35,16 @@ type AlertHistoryRenderItem = | { kind: 'alert'; key: string; alert: AlertHistoryAlert }; export function AlertHistoryTableSection(props: AlertHistoryTableSectionProps) { + // The card's own width decides, not the viewport: a laptop window with the + // Alerts rail open leaves the table far less room than the screen suggests. + // Until the card is measured, the phone breakpoint decides as before. + const cardWidth = useObservedElementWidth(); + const { isMobile } = useBreakpoint(); + // A memo, so readers hear about a change of layout, not every resized pixel. + const showCards = createMemo(() => { + const width = cardWidth.width(); + return width === null ? isMobile() : width < ALERT_HISTORY_TABLE_MIN_WIDTH; + }); const renderItems = createMemo(() => { const items: AlertHistoryRenderItem[] = []; for (const [groupIndex, group] of props.state.groupedAlerts().entries()) { @@ -52,9 +71,12 @@ export function AlertHistoryTableSection(props: AlertHistoryTableSectionProps) { } > - <> - -