Merge remote-tracking branch 'origin/main' into fix/tablet-header-clips

This commit is contained in:
rcourtman
2026-10-06 09:18:09 +01:00
7 changed files with 264 additions and 29 deletions
@@ -1081,6 +1081,15 @@ presentation and sort below warnings, while the history facet exposes a real
Info option whose count and filtered rows use the same predicate as every
other severity.
The desktop alert history table sizes each column for its content instead of
giving all visible columns an equal share. Timestamp, severity, duration,
status, type and node get what their content needs (status fits
"acknowledged"), Resource and Node keep at least their old share, the two row
actions stay within two lines (one line from an 80rem card), and Message, the
column that says what happened, takes the rest. Equal shares held Message to
the same 100-160px as the timestamp, so at most a fifth of messages read in
full at any width; from a 960px viewport a third to four fifths now do.
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
@@ -5872,6 +5872,11 @@ not overlap or create horizontal overflow. The 390px operator qualification
must assert three visible labels, non-overlapping client rectangles, keyboard
reachability, and a contained document before its actual-pixels receipt is
recorded.
The Alert History table's density tracks in `index.css` carry per-column widths
through `--alert-history-<column>-width` custom properties set on the table at
each density. Fixed layout sizes all nine tracks before collapsed ones are
removed, so the visible widths sum to 100% and the table width is the sum of
all nine; `App.architecture.test.ts` checks both at every density.
Alert History investigation detail is also responsive by interaction model,
not only by CSS. Desktop may keep Timeline and Resource incident detail inline
with its table row, while the virtualized phone card list must open the shared
@@ -0,0 +1,91 @@
{
"version": 1,
"base_sha": "1dcf9418aae87f9427dbfc6bcae67dbde114bf8a",
"verified_at": "2026-10-05T20:48:44Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/features/alerts/AlertHistoryTableSection.tsx",
"frontend-modern/src/index.css"
],
"content_sha256": {
"frontend-modern/src/features/alerts/AlertHistoryTableSection.tsx": "13521f02ee132d188c1b5f57639f5b73faba81bab3e9f9abd9f77926236352b8",
"frontend-modern/src/index.css": "e4bd257dd742fa95c9ab360b98348b63249ff63f8db4d85e4c5d4ee158f4bcf9"
},
"routes": [
"/alerts/history"
],
"viewports": [
{
"width": 768,
"height": 900
},
{
"width": 820,
"height": 900
},
{
"width": 900,
"height": 900
},
{
"width": 960,
"height": 900
},
{
"width": 1000,
"height": 900
},
{
"width": 1024,
"height": 900
},
{
"width": 1100,
"height": 900
},
{
"width": 1180,
"height": 900
},
{
"width": 1280,
"height": 900
},
{
"width": 1366,
"height": 900
},
{
"width": 1440,
"height": 900
},
{
"width": 1490,
"height": 900
},
{
"width": 1600,
"height": 900
},
{
"width": 1760,
"height": 900
},
{
"width": 1920,
"height": 900
}
],
"states": [
"Private mock stack (PULSE_MOCK_MODE, random metrics off) serving this tree on main 1dcf9418aa; before = main, after = main plus this change, 106 alert rows measured in the same session; the table shows from 768 (below it the mobile list renders and is unchanged)",
"Main gave every visible column an equal share (16.7%, 14.3%, 11.1%), so Message got the same 100-160px as Timestamp: messages fully readable (each character checked against its clipping box) were 0-21 of 106 at every width; patched they rise at every width except the 600px card (1024 viewport), where neither shows any in full, for example 768 0 to 4, 1280 8 to 39, 1440 15 to 66, 1760 17 to 83, 1920 21 to 87",
"Status acknowledged (98px) overflowed its content box at 768, 1024 and 1100 on main (cut 6px at 1024); patched every status, type and severity pill fits at every width",
"No column is worse: Resource and Node clip counts are identical to main at every width, no header clips at 768, 1024, 1280, 1490, 1600 or 1760, and median row height is unchanged except 75 to 39px from 1760, where the two row actions fit on one line",
"Card width per density on this layout: 645 at 768, 600 at 1024 (navigation rail), 835 at 1280, 982 at 1440, 1134 at 1600, 1285 at 1760, 1436 at 1920; the visible tracks fill the card exactly at every width (table width equals the card)",
"Before and after screenshots of the table at 1024, 1440 and 1920"
],
"interactions": [
"Loaded /alerts/history fresh, waited for the rows, resized through every viewport before measuring",
"Opened and closed the Timeline and the Resource incident panels from the first row at 1024, 1280 and 1760; both open inline and close; no console errors"
]
}
@@ -1116,3 +1116,63 @@ describe('Docker phone update reflow boundary', () => {
);
});
});
describe('Alert History column widths', () => {
// Fixed layout sizes all nine tracks against the table width before the
// collapsed ones are removed, so at each density the visible tracks must sum
// to 100% and the table width must be the sum of all nine; otherwise the
// visible columns stop filling the card or overflow it.
const columns = [
'timestamp',
'resource',
'type',
'severity',
'message',
'duration',
'status',
'node',
'actions',
] as const;
const densities: Array<{ name: string; visible: readonly (typeof columns)[number][] }> = [
{
name: 'base',
visible: ['timestamp', 'resource', 'severity', 'message', 'status', 'actions'],
},
{
name: '50rem',
visible: ['timestamp', 'resource', 'severity', 'message', 'duration', 'status', 'actions'],
},
{ name: '64rem', visible: columns },
{ name: '80rem', visible: columns },
];
const blocks = [
...appStylesSource.matchAll(
/table\.alert-history-responsive-table \{\s*((?:--alert-history-[a-z]+-width: [\d.]+%;\s*)+)width: ([\d.]+)%;/g,
),
];
it('declares every track width at each density', () => {
expect(blocks).toHaveLength(densities.length);
for (const column of columns) {
expect(appStylesSource).toContain(
`col.alert-history-${column}-track {\n width: var(--alert-history-${column}-width);`,
);
}
});
it('fills the card exactly with the visible tracks', () => {
blocks.forEach((block, index) => {
const widths = Object.fromEntries(
[...block[1].matchAll(/--alert-history-([a-z]+)-width: ([\d.]+)%;/g)].map((match) => [
match[1],
Number(match[2]),
]),
);
expect(Object.keys(widths).sort()).toEqual([...columns].sort());
const visibleSum = densities[index].visible.reduce((sum, column) => sum + widths[column], 0);
const allSum = columns.reduce((sum, column) => sum + widths[column], 0);
expect(visibleSum).toBeCloseTo(100, 6);
expect(Number(block[2])).toBeCloseTo(allSum, 6);
});
});
});
@@ -54,16 +54,17 @@ export function AlertHistoryTableSection(props: AlertHistoryTableSectionProps) {
<AlertHistoryMobileList state={props.state} />
<TableCard class="mb-2 hidden md:block">
<Table class="alert-history-responsive-table table-fixed min-w-0 text-[11px] sm:text-sm">
{/* Each track pairs its density visibility with its own width (index.css). */}
<colgroup>
<col class="alert-history-core-track" />
<col class="alert-history-core-track" />
<col class="alert-history-full-detail-track" />
<col class="alert-history-core-track" />
<col class="alert-history-core-track" />
<col class="alert-history-context-track" />
<col class="alert-history-core-track" />
<col class="alert-history-full-detail-track" />
<col class="alert-history-core-track" />
<col class="alert-history-core-track alert-history-timestamp-track" />
<col class="alert-history-core-track alert-history-resource-track" />
<col class="alert-history-full-detail-track alert-history-type-track" />
<col class="alert-history-core-track alert-history-severity-track" />
<col class="alert-history-core-track alert-history-message-track" />
<col class="alert-history-context-track alert-history-duration-track" />
<col class="alert-history-core-track alert-history-status-track" />
<col class="alert-history-full-detail-track alert-history-node-track" />
<col class="alert-history-core-track alert-history-actions-track" />
</colgroup>
<TableHeader>
<TableRow class="border-b border-border bg-surface-hover text-muted">
+84 -20
View File
@@ -498,7 +498,13 @@
at 800px, while type and node remain reserved for a 1024px table shell.
Fixed-layout tables calculate all nine tracks before collapsed columns are
removed, so the table width compensates at each density to let the six,
seven, or nine visible tracks fill the card without creating a scrollbar.
seven, or nine visible tracks fill the card without creating a scrollbar:
it is the sum of all nine track widths over the sum of the visible ones.
Visible tracks sum to 100% and are sized for the narrowest card at each
density. Timestamp, severity, duration, status, type and node get what
their content needs (status fits "acknowledged"), actions keep their two
buttons in at most two lines, and message, the column that says what
happened, takes most of what is left. At 80rem actions fit on one line.
*/
.table-scroll-shell col.alert-history-context-track,
.table-scroll-shell col.alert-history-full-detail-track {
@@ -510,14 +516,53 @@
visibility: collapse;
}
.table-scroll-shell col.alert-history-core-track,
.table-scroll-shell col.alert-history-context-track,
.table-scroll-shell col.alert-history-full-detail-track {
width: 16.6667%;
.table-scroll-shell table.alert-history-responsive-table {
--alert-history-timestamp-width: 15.6%;
--alert-history-resource-width: 16.7%;
--alert-history-type-width: 11.8%;
--alert-history-severity-width: 13.6%;
--alert-history-message-width: 18.2%;
--alert-history-duration-width: 11.5%;
--alert-history-status-width: 19.2%;
--alert-history-node-width: 11.2%;
--alert-history-actions-width: 16.7%;
width: 134.5%;
}
.table-scroll-shell table.alert-history-responsive-table {
width: 150%;
.table-scroll-shell col.alert-history-timestamp-track {
width: var(--alert-history-timestamp-width);
}
.table-scroll-shell col.alert-history-resource-track {
width: var(--alert-history-resource-width);
}
.table-scroll-shell col.alert-history-type-track {
width: var(--alert-history-type-width);
}
.table-scroll-shell col.alert-history-severity-track {
width: var(--alert-history-severity-width);
}
.table-scroll-shell col.alert-history-message-track {
width: var(--alert-history-message-width);
}
.table-scroll-shell col.alert-history-duration-track {
width: var(--alert-history-duration-width);
}
.table-scroll-shell col.alert-history-status-track {
width: var(--alert-history-status-width);
}
.table-scroll-shell col.alert-history-node-track {
width: var(--alert-history-node-width);
}
.table-scroll-shell col.alert-history-actions-track {
width: var(--alert-history-actions-width);
}
/*
@@ -578,14 +623,17 @@
visibility: visible;
}
.table-scroll-shell col.alert-history-core-track,
.table-scroll-shell col.alert-history-context-track,
.table-scroll-shell col.alert-history-full-detail-track {
width: 14.2857%;
}
.table-scroll-shell table.alert-history-responsive-table {
width: 128.5714%;
--alert-history-timestamp-width: 11.8%;
--alert-history-resource-width: 14.3%;
--alert-history-type-width: 11.8%;
--alert-history-severity-width: 10.2%;
--alert-history-message-width: 23.4%;
--alert-history-duration-width: 11.5%;
--alert-history-status-width: 14.5%;
--alert-history-node-width: 11.2%;
--alert-history-actions-width: 14.3%;
width: 123%;
}
.table-scroll-shell :is(th, td).agent-doctor-target-column {
@@ -619,18 +667,34 @@
visibility: visible;
}
.table-scroll-shell col.alert-history-core-track,
.table-scroll-shell col.alert-history-context-track,
.table-scroll-shell col.alert-history-full-detail-track {
width: 11.1111%;
}
.table-scroll-shell table.alert-history-responsive-table {
--alert-history-timestamp-width: 9.3%;
--alert-history-resource-width: 11.2%;
--alert-history-type-width: 11.8%;
--alert-history-severity-width: 8%;
--alert-history-message-width: 17%;
--alert-history-duration-width: 9%;
--alert-history-status-width: 11.3%;
--alert-history-node-width: 11.2%;
--alert-history-actions-width: 11.2%;
width: 100%;
}
}
@container (min-width: 80rem) {
.table-scroll-shell table.alert-history-responsive-table {
--alert-history-timestamp-width: 7.6%;
--alert-history-resource-width: 11.2%;
--alert-history-type-width: 9.5%;
--alert-history-severity-width: 6.5%;
--alert-history-message-width: 23.3%;
--alert-history-duration-width: 7.3%;
--alert-history-status-width: 9.2%;
--alert-history-node-width: 11.2%;
--alert-history-actions-width: 14.2%;
width: 100%;
}
.table-scroll-shell :is(th, td).hidden.xl\:table-cell.agent-machine-address-column {
display: table-cell;
}
@@ -522,6 +522,11 @@ describe('tab path helpers', () => {
expect(alertHistoryTableSectionSource).toMatch(/<Table(?:\s|>)/);
expect(alertHistoryTableSectionSource).toContain('alert-history-responsive-table');
expect(alertHistoryTableSectionSource).toContain('<colgroup>');
// Each track carries its own width so message, the column that says what
// happened, is not held to the same share as the timestamp.
expect(alertHistoryTableSectionSource).toMatch(
/alert-history-timestamp-track[\s\S]*alert-history-resource-track[\s\S]*alert-history-type-track[\s\S]*alert-history-severity-track[\s\S]*alert-history-message-track[\s\S]*alert-history-duration-track[\s\S]*alert-history-status-track[\s\S]*alert-history-node-track[\s\S]*alert-history-actions-track/,
);
expect(alertHistoryTableSectionSource).toContain('alert-history-timestamp-column');
expect(alertHistoryTableSectionSource).toContain('alert-history-full-detail-column');
expect(alertHistoryTableAlertRowSource).toContain('alert-history-timestamp-column');