diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md
index ed5a3b0e3..a47e21132 100644
--- a/docs/release-control/v6/internal/subsystems/alerts.md
+++ b/docs/release-control/v6/internal/subsystems/alerts.md
@@ -2647,3 +2647,20 @@ The hook and destinations caller regressions in
`useAlertDestinationsTabState.test.tsx` pin ordering and loading ownership.
`scripts/check-delivery-health-ordering.mjs` exercises the real caller and card
in Chromium with scripted API completions; it is not installed delivery proof.
+
+### Alert status distinguishes dispatch from destination evidence
+
+The active alert card renders a valid diagnosis `lastNotified` timestamp as
+“Dispatch requested”, never “Notified”: the alert manager records this field
+before invoking delivery callbacks. A cooldown's `nextEligibleAt` is labelled
+“next eligible”, not a promised send time. Neither field proves destination
+acceptance or recipient receipt; that evidence must not be inferred from the
+muted presentation tone. Missing or invalid timestamps retain the existing
+pending/cooldown fallback; acknowledged alerts retain their badge without a
+second status line. No API field, notification policy or shared primitive changes.
+
+The existing wrapping status text must remain readable at desktop and phone
+widths despite the longer labels. The presentation and Overview delivery-status
+tests cover the evidence boundary; `scripts/check-alert-dispatch-copy.mjs`
+qualifies the real Overview with scripted API data in Chromium, not installed
+notification delivery.
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 47f916c66..19d0e4991 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -7154,3 +7154,20 @@ confirmation and wrapping controls remain unchanged; no new primitive is added.
The focused hook/caller tests and `scripts/check-delivery-health-ordering.mjs`
cover this dependency at desktop and narrow widths using scripted health and
queue-action responses, without claiming backend notification delivery.
+
+### Alert status distinguishes dispatch from destination evidence
+
+The active alert card renders a valid diagnosis `lastNotified` timestamp as
+“Dispatch requested”, never “Notified”: the alert manager records this field
+before invoking delivery callbacks. A cooldown's `nextEligibleAt` is labelled
+“next eligible”, not a promised send time. Neither field proves destination
+acceptance or recipient receipt; that evidence must not be inferred from the
+muted presentation tone. Missing or invalid timestamps retain the existing
+pending/cooldown fallback; acknowledged alerts retain their badge without a
+second status line. No API field, notification policy or shared primitive changes.
+
+The existing wrapping status text must remain readable at desktop and phone
+widths despite the longer labels. The presentation and Overview delivery-status
+tests cover the evidence boundary; `scripts/check-alert-dispatch-copy.mjs`
+qualifies the real Overview with scripted API data in Chromium, not installed
+notification delivery.
diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json
index a942fbaa7..5e8b35ad1 100644
--- a/frontend-modern/browser-verification.json
+++ b/frontend-modern/browser-verification.json
@@ -1,18 +1,16 @@
{
"version": 1,
- "base_sha": "9b4fa52d84e6196108e961ccba2b317df497268b",
- "verified_at": "2026-09-06T10:15:18.709765Z",
+ "base_sha": "4fd68c72b26f033f5b1b0fcb7d7b2b0d67e85ec8",
+ "verified_at": "2026-09-06T14:12:06.315693Z",
"result": "passed",
"changed_paths": [
- "frontend-modern/src/components/AI/Chat/ChatMessages.tsx",
- "frontend-modern/src/components/AI/Chat/hooks/useChat.ts"
+ "frontend-modern/src/features/alerts/deliveryDiagnosisPresentation.ts"
],
"content_sha256": {
- "frontend-modern/src/components/AI/Chat/ChatMessages.tsx": "9672f7608d1e3a531c73cba20fd4a78752316783212afd0c292ddfd11d2bf371",
- "frontend-modern/src/components/AI/Chat/hooks/useChat.ts": "0b56b7a56e35d51ca96f0e126dd493b3164aa9e0ad4d8ae24bcf3af7a574b97c"
+ "frontend-modern/src/features/alerts/deliveryDiagnosisPresentation.ts": "bc3eadf8f790517b377430a42eb67e8fdfcadb53bb13b87cd971d6a9ab91d607"
},
"routes": [
- "/patrol"
+ "/qualification (isolated real OverviewTab, not installed /alerts)"
],
"viewports": [
{
@@ -29,12 +27,9 @@
}
],
"states": [
- "Captured healthy and dependency Assistant responses replayed without provider calls. All 7 and 16 terminal tool records retain exact inputs, outputs and distinct rows. Success, failed NO_AGENT and unsupported config results inspected.",
- "Controlled stream: concurrent same-name calls, repeated starts, progress, two separate approval cards, sibling completion, canceled call and failed completion. No real action was attempted.",
- "Restored persisted healthy and dependency sessions retain original prompts and evidence. Artificial route warning reflects blocked non-GET readiness checks, not route qualification."
+ "Scripted ready and cooldown diagnoses with lastNotified render Dispatch requested; cooldown says next eligible. Missing timestamp remains Notification pending. No Notified label."
],
"interactions": [
- "Hover, focus, Enter/Space expand and collapse, scroll expanded output to its end, Escape, reload and select the original persisted session. Tool details and wrapping inspected at desktop, intermediate and narrow widths.",
- "Private evidence: /Volumes/Development/pulse/tmp/patrol-assistant-lab-readiness/identity-verified-healthy, identity-verified-fault and identity-states. Baseline reproductions retained in identity-before-fault and failed focused regressions."
+ "Loaded real Overview in Chromium via scripts/check-alert-dispatch-copy.mjs; asserted status text ranges fit each viewport and no page errors. Inspected desktop and phone screenshots. No delivery actions invoked; no backend receipt claimed. Screenshots retained in lane outcome evidence."
]
}
diff --git a/frontend-modern/src/features/alerts/__tests__/OverviewTab.deliverystatus.test.tsx b/frontend-modern/src/features/alerts/__tests__/OverviewTab.deliverystatus.test.tsx
index 995a149a4..631ae1590 100644
--- a/frontend-modern/src/features/alerts/__tests__/OverviewTab.deliverystatus.test.tsx
+++ b/frontend-modern/src/features/alerts/__tests__/OverviewTab.deliverystatus.test.tsx
@@ -111,6 +111,19 @@ describe('OverviewTab delivery status line', () => {
expect(getDeliveryDiagnoses).toHaveBeenCalled();
});
+ it.each([
+ { status: 'would_send', reason: 'ready' },
+ { status: 'suppressed', reason: 'cooldown', nextEligibleAt: '2026-08-26T10:20:00Z' },
+ ] as const)('does not turn dispatch evidence into receipt for $reason', async (state) => {
+ getDeliveryDiagnoses.mockResolvedValue([
+ makeDiagnosis('a1', { ...state, lastNotified: '2026-08-26T10:15:00Z' }),
+ ]);
+ render(() => );
+ await waitFor(() => expect(screen.getByText(/^Dispatch requested /)).toBeTruthy());
+ expect(screen.queryByText(/^Notified /)).toBeNull();
+ if (state.reason === 'cooldown') expect(screen.getByText(/next eligible/)).toBeTruthy();
+ });
+
it('renders no delivery line when the diagnosis fetch fails', async () => {
const activeAlerts: Record = { a1: makeAlert('a1') };
getDeliveryDiagnoses.mockRejectedValue(new Error('boom'));
diff --git a/frontend-modern/src/features/alerts/__tests__/OverviewTab.total24h.test.tsx b/frontend-modern/src/features/alerts/__tests__/OverviewTab.total24h.test.tsx
index dedd034d3..1309b153d 100644
--- a/frontend-modern/src/features/alerts/__tests__/OverviewTab.total24h.test.tsx
+++ b/frontend-modern/src/features/alerts/__tests__/OverviewTab.total24h.test.tsx
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { cleanup, render, screen } from '@solidjs/testing-library';
+import { cleanup, render, screen, waitFor } from '@solidjs/testing-library';
import { DEFAULT_LOCALE, setActiveLocale } from '@/i18n';
import type { Alert, AlertDeliveryDiagnosis } from '@/types/api';
@@ -75,6 +75,37 @@ describe('OverviewTab Last 24 Hours stat', () => {
setActiveLocale(DEFAULT_LOCALE);
});
+ it('keeps dispatch evidence separate from the triggered count', async () => {
+ vi.useRealTimers();
+ const now = Date.now();
+ getDeliveryDiagnoses.mockResolvedValue([
+ {
+ alertIdentifier: 'old',
+ alertId: 'old',
+ status: 'would_send',
+ reason: 'ready',
+ lastNotified: new Date(now).toISOString(),
+ } as AlertDeliveryDiagnosis,
+ ]);
+ render(() => (
+
+ ));
+ await waitFor(() => expect(screen.getByText(/^Dispatch requested /)).toBeTruthy());
+ expect(screen.queryByText(/^Notified /)).toBeNull();
+ expect(
+ screen
+ .getByText('Triggered (24h)')
+ .closest('tr')
+ ?.querySelector('[data-testid="alert-overview-stat-value"]')?.textContent,
+ ).toBe('0');
+ });
+
it('counts only alerts with startTime within the last 24 hours', () => {
const now = Date.now();
const oneHourAgo = new Date(now - 3_600_000).toISOString();
diff --git a/frontend-modern/src/features/alerts/__tests__/deliveryDiagnosisPresentation.test.ts b/frontend-modern/src/features/alerts/__tests__/deliveryDiagnosisPresentation.test.ts
index ecc7be09c..c5c4f53fd 100644
--- a/frontend-modern/src/features/alerts/__tests__/deliveryDiagnosisPresentation.test.ts
+++ b/frontend-modern/src/features/alerts/__tests__/deliveryDiagnosisPresentation.test.ts
@@ -35,13 +35,45 @@ describe('describeAlertDeliveryStatus', () => {
expect(describeAlertDeliveryStatus(diagnosis, true)).toBeNull();
});
- it('shows the notified time when eligible and already notified', () => {
+ it('shows dispatch evidence without claiming destination success', () => {
const diagnosis = baseDiagnosis({ lastNotified: '2026-08-26T10:15:00Z' });
const line = describeAlertDeliveryStatus(diagnosis, false);
expect(line?.tone).toBe('muted');
- expect(line?.label).toMatch(/^Notified /);
+ expect(line?.label).toMatch(/^Dispatch requested /);
});
+ it('shows dispatch without promising another send when cooldown has no next time', () => {
+ const line = describeAlertDeliveryStatus(
+ baseDiagnosis({
+ status: 'suppressed',
+ reason: 'cooldown',
+ lastNotified: '2026-08-26T10:15:00Z',
+ }),
+ false,
+ );
+ expect(line?.label).toMatch(/^Dispatch requested /);
+ expect(line?.label).not.toContain('next');
+ });
+
+ it.each([undefined, '', 'invalid'])(
+ 'does not invent dispatch for timestamp %s',
+ (lastNotified) => {
+ expect(describeAlertDeliveryStatus(baseDiagnosis({ lastNotified }), false)?.label).toBe(
+ 'Notification pending',
+ );
+ expect(
+ describeAlertDeliveryStatus(
+ baseDiagnosis({
+ status: 'suppressed',
+ reason: 'cooldown',
+ lastNotified,
+ }),
+ false,
+ )?.label,
+ ).toBe('Waiting for cooldown');
+ },
+ );
+
it('shows pending when eligible but never notified', () => {
const line = describeAlertDeliveryStatus(baseDiagnosis({}), false);
expect(line).toEqual({ label: 'Notification pending', tone: 'muted' });
@@ -67,7 +99,7 @@ describe('describeAlertDeliveryStatus', () => {
});
const line = describeAlertDeliveryStatus(diagnosis, false);
expect(line?.tone).toBe('muted');
- expect(line?.label).toMatch(/^Notified .* — next /);
+ expect(line?.label).toMatch(/^Dispatch requested .* — next eligible /);
});
it.each([
diff --git a/frontend-modern/src/features/alerts/deliveryDiagnosisPresentation.ts b/frontend-modern/src/features/alerts/deliveryDiagnosisPresentation.ts
index b5ca69e95..33db25b55 100644
--- a/frontend-modern/src/features/alerts/deliveryDiagnosisPresentation.ts
+++ b/frontend-modern/src/features/alerts/deliveryDiagnosisPresentation.ts
@@ -54,9 +54,11 @@ export const describeAlertDeliveryStatus = (
const reason = (diagnosis.reason || '').split(':')[0];
+ // lastNotified is recorded before delivery callbacks; it is dispatch evidence,
+ // not confirmation that a destination accepted or a person received anything.
if (diagnosis.status === 'would_send') {
const notifiedAt = formatShortTime(diagnosis.lastNotified);
- if (notifiedAt) return { label: `Notified ${notifiedAt}`, tone: 'muted' };
+ if (notifiedAt) return { label: `Dispatch requested ${notifiedAt}`, tone: 'muted' };
return { label: 'Notification pending', tone: 'muted' };
}
@@ -75,9 +77,12 @@ export const describeAlertDeliveryStatus = (
const notifiedAt = formatShortTime(diagnosis.lastNotified);
const nextAt = formatShortTime(diagnosis.nextEligibleAt);
if (notifiedAt && nextAt) {
- return { label: `Notified ${notifiedAt} — next ${nextAt}`, tone: 'muted' };
+ return {
+ label: `Dispatch requested ${notifiedAt} — next eligible ${nextAt}`,
+ tone: 'muted',
+ };
}
- if (notifiedAt) return { label: `Notified ${notifiedAt}`, tone: 'muted' };
+ if (notifiedAt) return { label: `Dispatch requested ${notifiedAt}`, tone: 'muted' };
return { label: 'Waiting for cooldown', tone: 'muted' };
}
case 'rate_limited':
diff --git a/scripts/check-alert-dispatch-copy.mjs b/scripts/check-alert-dispatch-copy.mjs
new file mode 100644
index 000000000..c3daecccb
--- /dev/null
+++ b/scripts/check-alert-dispatch-copy.mjs
@@ -0,0 +1,123 @@
+// Isolated real-browser component qualification; no installed backend or delivery claim.
+import { createServer } from "../frontend-modern/node_modules/vite/dist/node/index.js";
+import solid from "../frontend-modern/node_modules/vite-plugin-solid/dist/esm/index.mjs";
+import { chromium } from "@playwright/test";
+import { resolve } from "node:path";
+import { mkdirSync } from "node:fs";
+import assert from "node:assert/strict";
+const root = resolve("frontend-modern");
+process.chdir(root);
+const fixture = `
+import { render } from 'solid-js/web';
+import { Router, Route } from '@solidjs/router';
+import { AlertsAPI } from '/src/api/alerts';
+import { NotificationsAPI } from '/src/api/notifications';
+import { OverviewTab } from '/src/features/alerts/OverviewTab';
+import '/src/index.css';
+const ids = ['ready','cooldown','pending'];
+AlertsAPI.getDeliveryDiagnoses = async () => ids.map(id => ({
+alertIdentifier:id, alertId:id, trackingKey:id, status:id==='cooldown'?'suppressed':'would_send',
+reason:id==='cooldown'?'cooldown':'ready', lastNotified:id==='pending'?undefined:'2026-08-26T10:15:00Z',
+nextEligibleAt:id==='cooldown'?'2026-08-26T10:20:00Z':undefined
+}));
+AlertsAPI.getEvents = async () => [];
+NotificationsAPI.getHealth = async () => ({queue:{status:'healthy'}});
+const alerts = Object.fromEntries(ids.map(id => [id, {id,resourceId:id,resourceName:'VM '+id,
+type:'cpu',level:'warning',message:'High CPU on '+id,startTime:'2026-08-26T10:00:00Z',acknowledged:false,node:'node1'}]));
+function Fixture() { return {}} showQuickTip={()=>false} dismissQuickTip={()=>{}} showAcknowledged={()=>true}
+setShowAcknowledged={()=>{}} alertsDisabled={()=>false}/>; }
+render(()=>,document.getElementById('root'));
+`;
+const server = await createServer({
+ root,
+ configFile: false,
+ optimizeDeps: {
+ noDiscovery: true,
+ entries: [],
+ esbuildOptions: { target: "esnext" },
+ },
+ esbuild: { target: "esnext" },
+ plugins: [
+ solid(),
+ {
+ name: "dispatch-fixture",
+ configureServer(s) {
+ s.middlewares.use((req, res, next) => {
+ if (req.url === "/qualification") {
+ res.setHeader("Content-Type", "text/html");
+ res.end(
+ '',
+ );
+ } else next();
+ });
+ },
+ resolveId(id) {
+ if (id === "/dispatch-fixture.tsx") return id;
+ },
+ load(id) {
+ if (id === "/dispatch-fixture.tsx") return fixture;
+ },
+ },
+ ],
+ resolve: { alias: { "@": resolve(root, "src") } },
+ server: { host: "127.0.0.1", port: 5198, strictPort: true },
+});
+let browser;
+try {
+ await server.listen();
+ browser = await chromium.launch({ headless: true });
+ mkdirSync("/tmp/pulse-alert-dispatch", { recursive: true });
+ for (const width of [1440, 900, 390]) {
+ const page = await browser.newPage({ viewport: { width, height: 1000 } });
+ const errors = [];
+ page.on("pageerror", (e) => {
+ errors.push(e.message);
+ console.error(e.message);
+ });
+ page.on("console", (m) => {
+ if (m.type() === "error") console.error(m.text());
+ });
+ await page.route("http://127.0.0.1:5198/api/**", (route) =>
+ route.fulfill({ json: [] }),
+ );
+ await page.goto("http://127.0.0.1:5198/qualification");
+ await page.getByText(/^Dispatch requested .*next eligible/).waitFor();
+ assert.equal(await page.getByText(/^Dispatch requested /).count(), 2);
+ assert.equal(await page.getByText(/^Notified /).count(), 0);
+ assert.equal(
+ await page.getByText("Notification pending", { exact: true }).count(),
+ 1,
+ );
+ for (const label of await page.getByText(/^Dispatch requested /).all()) {
+ assert.equal(
+ await label.evaluate((el) => {
+ const r = document.createRange();
+ r.selectNodeContents(el);
+ return [...r.getClientRects()].every(
+ (b) => b.left >= 0 && b.right <= innerWidth,
+ );
+ }),
+ true,
+ "status text must fit viewport",
+ );
+ }
+ assert.deepEqual(errors, []);
+ await page.screenshot({
+ path: "/tmp/pulse-alert-dispatch/" + width + ".png",
+ fullPage: true,
+ });
+ await page.close();
+ }
+ console.log(
+ JSON.stringify({
+ result: "passed",
+ viewports: [1440, 900, 390],
+ scope:
+ "Real Overview and Chromium; scripted diagnoses, not installed delivery or receipt",
+ }),
+ );
+} finally {
+ await browser?.close();
+ await server.close();
+}