fix(alerts): align the Started run with the alert-card footer (#2119)

The Alerts overview alert-card footer is an items-center flex row whose first child, the Started timestamp, carried its own mt-1. Under items-center that margin shifted the Started run 2px below the adjacent delivery-status run, which is the small text misalignment the reporter underlined in #2119. Remove the child margin; the footer row already carries the top margin, so the two runs share a baseline.

Add a presentation regression test and an offline browser proof that measures the text rects of both runs at desktop and narrow widths.

Contract-Neutral: Behavioral footer-alignment fix; no public-contract or subsystem-shape delta.
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot] 2026-09-21 14:31:27 +01:00
parent eb47a4e653
commit 41fa79d52b
6 changed files with 205 additions and 30 deletions

View file

@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/browser-tests/alert-card-footer.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,43 @@
// Browser fixture for issue #2119: the Alerts overview alert-card footer.
//
// It reproduces the production footer from AlertOverviewAlertCard.tsx: a flex
// row (items-center) whose first child is the "Started" line classed by
// getAlertOverviewStartedAtClass(), followed by the delivery-status span. The
// reporter's screenshot underlines the Started timestamp and the status text at
// different heights, so the fixture exists to measure whether the two runs share
// a baseline. Synthetic props only; no backend, API or WebSocket path.
import { render } from 'solid-js/web';
import { getAlertOverviewStartedAtClass } from '../src/utils/alertOverviewPresentation';
import '../src/index.css';
function AlertCardFooter() {
return (
<div
id="alert-card"
style={{
width: '960px',
margin: '24px',
padding: '16px',
border: '1px solid #f59e0b',
'border-radius': '8px',
background: '#111827',
color: '#9ca3af',
}}
>
<p style={{ margin: '0', 'font-size': '14px', color: '#e5e7eb' }}>
Alert notifications are not reaching their destinations.
</p>
<div id="alert-card-footer" class="flex flex-wrap items-center gap-x-3 gap-y-0.5 mt-1">
<p class={getAlertOverviewStartedAtClass()} data-testid="started">
Started: 9/18/2026, 12:15:58 PM
</p>
<span class="text-xs text-muted" data-testid="status">
Notified 12:15 PM
</span>
</div>
</div>
);
}
render(() => <AlertCardFooter />, document.getElementById('root')!);

View file

@ -1,48 +1,38 @@
{
"version": 1,
"base_sha": "fdb2be15befb4fbcd34b4e0399b147bfa098b355",
"verified_at": "2026-09-21T05:50:47Z",
"base_sha": "eb47a4e653581661d4b267710d9c1bb60a904733",
"verified_at": "2026-09-21T14:31:00Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/features/docs/docMarkdown.ts",
"frontend-modern/src/pages/Docs.tsx"
"frontend-modern/src/utils/alertOverviewPresentation.ts"
],
"content_sha256": {
"frontend-modern/src/features/docs/docMarkdown.ts": "e114a4c2e49a8dc0f2549a704af6ceac54d4eca0cfcae28c8ab8cb6cc1b0718c",
"frontend-modern/src/pages/Docs.tsx": "c6c4b066c6af8a4eb8f01a9e03c2f939ee1592867dea2bf0f328bf111f99f2eb"
"frontend-modern/src/utils/alertOverviewPresentation.ts": "7f68a70800deb0cb8214d4fd25c7454c5b0fde1084e08365eb93bc361b80846a"
},
"routes": [
"/docs/API",
"/docs/API#resource-maintenance-and-operator-state",
"/docs/README"
"/browser-tests/alert-card-footer.html"
],
"viewports": [
{
"width": 1440,
"height": 900
"width": 2010,
"height": 1072
},
{
"width": 390,
"height": 844
"width": 480,
"height": 800
}
],
"states": [
"API reference renders 149 headings, every one with a unique generated id; the maintenance section id is resource-maintenance-and-operator-state",
"The in-document link to #resource-maintenance-and-operator-state resolves to the generated heading",
"Direct fragment load focuses and scrolls the maintenance heading after the asynchronous Markdown fetch",
"Reload on the same fragment restores focus and target",
"Malformed (#%invalid) and missing (#does-not-exist) fragments leave the document rendered and do not move focus",
"Keyboard activation of the All documentation link routes to /docs/README inside the SPA shell",
"Wide API tables scroll inside their own container; neither viewport overflows horizontally"
"Alerts overview alert-card footer with the Started timestamp and the adjacent delivery-status run",
"The Started run classed by getAlertOverviewStartedAtClass() inside the items-center footer flex row",
"Desktop 2010x1072 and narrow 480x800 layouts"
],
"interactions": [
"Mount the production Docs page under the production router and serve the real public/docs/API.md asset from the Vite dev server",
"Load /docs/API, /docs/API#resource-maintenance-and-operator-state, a malformed fragment and a missing fragment, and reload the direct fragment",
"Assert document.activeElement, tabIndex, scrollIntoView position and window.location.hash",
"Focus the first documentation link and activate it with Enter, confirming router navigation without a full reload",
"Resize to 390x844 and check page overflow and table scroll wrappers",
"Record Chromium 141.0.7390.37 with no uncaught page errors and no failed responses"
"Mount the production footer markup from AlertOverviewAlertCard.tsx with the class returned by getAlertOverviewStartedAtClass() and the project CSS",
"Measure the text bounding rects of the Started run and the status run and assert their top and bottom edges match within 1px",
"Before the fix the same run reported the Started run 2px below the status run (started top 70 / bottom 86 vs status top 68 / bottom 84); after the fix both report top 66 / bottom 82 at both viewports",
"Inspect the desktop and narrow screenshots"
],
"command": "pulse-worker-browser scripts/check-docs-fragment-navigation.cjs",
"notes": "Offline Playwright Chromium run of the production Docs page. Synthetic backend: /api/security/status is answered with hasAuthentication=false; the shipped API markdown asset is served by the Vite dev server, not a live Pulse. Heading ids follow GitHub's algorithm (lower-case, punctuation removed, spaces to hyphens, per-document duplicate numbering) so repository fragment links resolve. The renderer keeps the documentation route dependency-free. Chromium 141.0.7390.37 matches the offline runtime. This receipt is content-addressed to the changed frontend sources at this candidate base."
"command": "pulse-worker-browser scripts/check-alert-card-footer.cjs",
"notes": "Offline Playwright Chromium 141.0.7390.37 run of the production Alerts overview alert-card footer markup with the class returned by getAlertOverviewStartedAtClass() and the project CSS; no backend, API or WebSocket path. The footer is an items-center flex row that already carries the top margin, so the child 'mt-1' pushed the Started run 2px below the delivery-status span; this matches the two runs the reporter underlined at different heights in #2119. Removing the child margin aligns both runs (top 66 / bottom 82 at desktop and narrow, delta 0). No page errors and no failed responses. This receipt is content-addressed to alertOverviewPresentation.ts sha256 7f68a708…."
}

View file

@ -324,7 +324,7 @@ describe('alertOverviewPresentation', () => {
expect(getAlertOverviewAcknowledgedBadgeClass()).toBe(
'px-2 py-0.5 text-xs bg-yellow-200 dark:bg-yellow-800 text-yellow-800 dark:text-yellow-200 rounded',
);
expect(getAlertOverviewStartedAtClass()).toBe('mt-1 text-xs text-muted');
expect(getAlertOverviewStartedAtClass()).toBe('text-xs text-muted');
expect(getAlertOverviewPrimaryActionClass(true)).toBe(
'px-3 py-1.5 text-xs font-medium border rounded-md transition-all disabled:opacity-50 disabled:cursor-not-allowed text-base-content border-border hover:bg-surface-hover',
);
@ -336,6 +336,16 @@ describe('alertOverviewPresentation', () => {
);
});
it('keeps the Started run on the footer baseline (#2119)', () => {
// The alert-card footer is an items-center flex row that already carries the
// top margin. A vertical margin on this child shifted the Started run below
// the adjacent delivery-status span, which the reporter underlined.
const startedClass = getAlertOverviewStartedAtClass();
expect(startedClass.split(/\s+/)).not.toContain('mt-1');
expect(startedClass).toContain('text-xs');
expect(startedClass).toContain('text-muted');
});
it('localizes alert overview helper copy through the active locale', () => {
setActiveLocale('es');

View file

@ -432,7 +432,9 @@ export function getAlertOverviewAcknowledgedBadgeClass(): string {
}
export function getAlertOverviewStartedAtClass(): string {
return 'mt-1 text-xs text-muted';
// The footer flex row already carries the top margin; a margin on this child
// shifts the Started run below its siblings under items-center (#2119).
return 'text-xs text-muted';
}
export function getAlertOverviewPrimaryActionClass(acknowledged: boolean): string {

View file

@ -0,0 +1,120 @@
// Offline real-browser regression for issue #2119 (Alerts overview card footer).
//
// Mounts the production footer markup with the class returned by
// getAlertOverviewStartedAtClass() and measures whether the "Started" run and
// the adjacent delivery-status run share a vertical baseline. Run with:
// pulse-worker-browser scripts/check-alert-card-footer.cjs
const path = require("node:path");
const { chromium } = require("playwright");
const ROOT = path.resolve(process.cwd(), "frontend-modern");
const launchOptions = {
headless: true,
channel: "chromium",
args: ["--no-sandbox"],
};
const measure = () =>
(() => {
const started = document.querySelector('[data-testid="started"]');
const status = document.querySelector('[data-testid="status"]');
if (!started || !status) return null;
const textRect = (el) => {
const range = document.createRange();
range.selectNodeContents(el);
const rect = range.getBoundingClientRect();
return { top: rect.top, bottom: rect.bottom };
};
const startedText = textRect(started);
const statusText = textRect(status);
return {
startedClass: started.getAttribute("class"),
statusClass: status.getAttribute("class"),
startedText,
statusText,
baselineDelta: Math.abs(startedText.bottom - statusText.bottom),
topDelta: Math.abs(startedText.top - statusText.top),
};
})();
(async () => {
process.chdir(ROOT);
const { createServer } = await import(
path.join(ROOT, "node_modules", "vite", "dist", "node", "index.js")
);
const server = await createServer({
root: ROOT,
configFile: path.join(ROOT, "vite.config.ts"),
server: { host: "127.0.0.1", port: 5198, strictPort: true },
});
let browser;
const errors = [];
const httpErrors = [];
try {
await server.listen();
browser = await chromium.launch(launchOptions);
const page = await browser.newPage({
viewport: { width: 2010, height: 1072 },
});
page.on("pageerror", (error) => errors.push(error.message));
page.on("console", (message) => {
if (message.type() !== "error") return;
if (message.text().includes("Failed to load resource")) return;
errors.push(`console: ${message.text()}`);
});
page.on("response", (response) => {
if (response.status() >= 400)
httpErrors.push(`${response.status()} ${response.url()}`);
});
await page.goto(
"http://127.0.0.1:5198/browser-tests/alert-card-footer.html",
);
await page.waitForSelector('[data-testid="started"]');
await page.waitForSelector('[data-testid="status"]');
const desktop = await page.evaluate(measure);
if (!desktop) throw new Error("footer runs not rendered");
await page.screenshot({
path: path.join(ROOT, "browser-tests", "alert-card-footer-desktop.png"),
});
await page.setViewportSize({ width: 480, height: 800 });
await page.waitForTimeout(150);
const narrow = await page.evaluate(measure);
await page.screenshot({
path: path.join(ROOT, "browser-tests", "alert-card-footer-narrow.png"),
});
if (errors.length > 0)
throw new Error(`page errors: ${errors.join(" | ")}`);
const worst = Math.max(
desktop.baselineDelta,
desktop.topDelta,
narrow.baselineDelta,
narrow.topDelta,
);
if (worst > 1) {
throw new Error(
`Started and status runs are vertically misaligned: desktop ${JSON.stringify(desktop)} ` +
`narrow ${JSON.stringify(narrow)}`,
);
}
console.log(
JSON.stringify(
{ result: "passed", desktop, narrow, httpErrors },
null,
2,
),
);
} finally {
if (browser) await browser.close();
await server.close();
}
})().catch((error) => {
console.error("FAILED:", error && error.stack ? error.stack : error);
process.exitCode = 1;
});