fix(web): identify notification destination server failures

The queue and health API expose server_error, but the delivery UI treated it as unclassified. Preserve that diagnosis and direct operators to service availability and server logs before retrying retained deliveries. Add focused label and health guidance regression coverage.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-08 12:51:40 +01:00
parent 550b2d80a1
commit 6988e486f2
5 changed files with 123 additions and 69 deletions
@@ -2813,3 +2813,18 @@ and fixture reset, overlap and unmount controls. It is component lifecycle
acceptance, not an installed full-page or notification-delivery receipt.
PR1973's proposed resourceIncidentError accessor is absent here; if introduced,
its writes must obey the same owner check and gain a stale-error regression.
### Destination server-error presentation
Delivery activity preserves the queue's `server_error` classification as
“Destination server error”, rather than treating it as unclassified. When that
class dominates retained terminal failures, the full health description directs
operators to destination service status and server logs, then to retry retained
deliveries once the service is available. Unknown classes still use the generic
fallback; this presentation does not change retry scheduling or imply receipt.
The presentation unit tests cover the label and terminal guidance.
`scripts/check-delivery-server-error-copy.mjs` renders the real health and log
cards in Chromium at desktop, tablet and phone widths, asserting the copy,
absence of page errors and horizontal overflow. Scripted props are component
evidence only, not installed notification delivery.
+10 -69
View File
@@ -1,67 +1,24 @@
{
"version": 1,
"base_sha": "701362ac25ef28dfa253f952820eed7c5a90db12",
"verified_at": "2026-09-08T02:20:17Z",
"base_sha": "550b2d80a175cc66ab4e7ac269b7ec350e101611",
"verified_at": "2026-09-08T11:54:21Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/AI/Chat/index.tsx",
"frontend-modern/src/components/Alerts/IncidentAssistantHandoffButton.tsx",
"frontend-modern/src/components/Alerts/IncidentTimelineEventCard.tsx",
"frontend-modern/src/components/Alerts/IncidentTimelinePanel.tsx",
"frontend-modern/src/components/Alerts/incidentAssistantHandoffModel.ts",
"frontend-modern/src/features/alerts/AlertHistoryItemActions.tsx",
"frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx",
"frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx",
"frontend-modern/src/features/alerts/MobileAlertHistoryInvestigationDialog.tsx",
"frontend-modern/src/features/alerts/useAlertHistoryState.ts",
"frontend-modern/src/features/alerts/useAlertResourceIncidentsState.ts",
"frontend-modern/src/hooks/useTypeToSearch.ts",
"frontend-modern/src/types/api.ts",
"frontend-modern/src/utils/alertIncidentPresentation.ts"
"frontend-modern/src/utils/alertDestinationsPresentation.ts"
],
"content_sha256": {
"frontend-modern/src/components/AI/Chat/index.tsx": "ca9e630294db426df591c3386d4fec161f34ade0bb6a8c4ea769130545a12341",
"frontend-modern/src/components/Alerts/IncidentAssistantHandoffButton.tsx": "b7a6ea6676ed9834d8254bb4ddab1301ec3b0d2368a96fa3e91e9ab5d2cefb4f",
"frontend-modern/src/components/Alerts/IncidentTimelineEventCard.tsx": "21e90602de00aa4765b1c40b5295a618a237501389d50ca9dbe36454abd11f32",
"frontend-modern/src/components/Alerts/IncidentTimelinePanel.tsx": "7298d4184c243325dbd671777257c7efd66c86091eb4b83663315e2bdc131fcb",
"frontend-modern/src/components/Alerts/incidentAssistantHandoffModel.ts": "9e777eee1be46f33ac146770fac6193cf6cd1c8b33ab0d0cd72fe57b98907a37",
"frontend-modern/src/features/alerts/AlertHistoryItemActions.tsx": "06e75d16ccad383b7c89d109baaee71852482f49b41ef3d0f5cc99768dd35101",
"frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx": "851a2c1dc11ccbc827a6a50dfe06088b13a7307ce489b0767804daf6f33d023c",
"frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx": "c6f89c4991c9cfc2b1c8c82f7fed35af93b3b0b29b16b2c4bdc61a51005bec4c",
"frontend-modern/src/features/alerts/MobileAlertHistoryInvestigationDialog.tsx": "55af36a7194c2dbe9d944ca08f52e794f2b5e2ccbe79427bc55abe66aaa8a522",
"frontend-modern/src/features/alerts/useAlertHistoryState.ts": "652a09b02321ad1cfd087aacc7937a6afb5cfb7da0e0d6ca997b30d5c9ac53bd",
"frontend-modern/src/features/alerts/useAlertResourceIncidentsState.ts": "7a4de5d23f84bffed0665edbc4c9bd7cf33120d81fc82f7e9f5d3df56cffeb45",
"frontend-modern/src/hooks/useTypeToSearch.ts": "b3f5244fd951baedc33fc8efee32c28077d94c07e5fce958eb6611bf50992a20",
"frontend-modern/src/types/api.ts": "82177cc67fc449a28d2d3cc226605ae091bd1f83fbdb001d15b3b430e5cd4100",
"frontend-modern/src/utils/alertIncidentPresentation.ts": "2e098b24b04c047a1b024a93608c5133720660cafc54c40408972413786a0a07"
"frontend-modern/src/utils/alertDestinationsPresentation.ts": "67f6f9a1c98345949e162bf06a38760e61457aa1e8a2130913646647a4f29e88"
},
"routes": [
"/alerts (retained upstream exact-content Chromium receipt for 13 unchanged runtime files)",
"/qualification (fresh combined hook and AlertResourceIncidentsPanel with scripted API)"
"/qualification (real health and delivery-log cards, scripted props)"
],
"viewports": [
{
"width": 1440,
"height": 1000
"height": 900
},
{
"width": 900,
"height": 1000
},
{
"width": 768,
"height": 1000
},
{
"width": 767,
"height": 1000
},
{
"width": 390,
"height": 1000
},
{
"width": 1440,
"height": 900
},
{
@@ -70,27 +27,11 @@
}
],
"states": [
"Canonical merged occurrence and source evidence, resolved alert with independently unknown current health.",
"Loading and disabled Refresh, initial/cached read failures, Retry/Refresh, successful empty, partial history and unknown times.",
"Saved operator note and unsaved draft across desktop/phone layout changes, long-note wrapping and persisted reload.",
"Timeline/resource handoff into Assistant, reopened composer focus, saved funded history-and-note response across widths.",
"Request reset, reverse completion, superseded success/failure, current failure, and success/failure after disposal.",
"Latest per-resource owner controls incident content, failed-read state, notifications, and loading completion.",
"Retry clears the current failed-read state while retaining cached history until the owning response succeeds.",
"Obsolete and disposed failures do not set the resource error accessor."
"Retained terminal server_error health diagnosis and failed delivery log entry"
],
"interactions": [
"Expand/collapse native Evidence details by keyboard, All/None event filters and note draft enable/clear.",
"Expand resource Events, inject read errors, retry and reload.",
"Resize source and open Assistant destination across 390/900/767/768/1440 widths.",
"Type in a modal without clearing background search, Escape and return focus to the selected card.",
"Resume the exact saved upstream session, scroll through its note explanation and inspect actual pixels.",
"Open a resource row, overlap refresh, reset or unmount, and resolve or reject deferred reads in controlled order."
"Load both cards at desktop, tablet and phone widths; assert label, guidance, no page errors or horizontal overflow."
],
"notes": "The protected upstream receipt remains exact for 13 unchanged runtime files. The sole combined-content change, useAlertResourceIncidentsState.ts, was freshly exercised with its real panel in 16 Chromium cases at desktop and phone widths after adding ownership to the upstream failed-read signal. Screenshots are retained under /tmp/pulse-incident-ownership. This is component lifecycle evidence, not an installed Alerts route, provider delivery, touch-device, production rollout, backup/restore, or population-reliability receipt.",
"command": "pulse-heavy-run -- node scripts/check-incident-request-ownership.mjs",
"limitations": [
"The 16-case qualification route uses fixture lifecycle controls and a scripted API; the supplied panel does not expose the error accessor directly, so that state is asserted through the fixture snapshot.",
"This is not installed acceptance, provider delivery, touch-device, production rollout, backup/restore, or population-reliability proof."
]
"notes": "Phone screenshot visually inspected; screenshots retained at /tmp/pulse-server-error-copy. Initial fixture omitted alertIds and timed out; corrected fixture passed all three widths. No installed delivery or receipt claim.",
"command": "pulse-heavy-run -- node scripts/check-delivery-server-error-copy.mjs"
}
@@ -236,9 +236,28 @@ describe('alert destinations delivery log copy', () => {
it('labels failure classes and falls back to unclassified for unknown values', () => {
expect(getAlertDeliveryLogFailureClassLabel('authentication')).toBe('Authentication failure');
expect(getAlertDeliveryLogFailureClassLabel('rate_limited')).toBe('Rate limited');
expect(getAlertDeliveryLogFailureClassLabel('server_error')).toBe('Destination server error');
expect(getAlertDeliveryLogFailureClassLabel('made-up-class')).toBe('Unclassified failure');
});
it('gives destination availability guidance for terminal server errors', () => {
const description = getAlertDestinationsDeliveryHealthDescription({
status: 'degraded',
failed: 1,
deadLetter: 0,
completedRetentionDays: 7,
deadLetterRetentionDays: 30,
failureClasses7d: { server_error: 1 },
failureClassesAvailable: true,
});
expect(description).toContain('classified as server error (1)');
expect(description).toContain(
'Check the destination service status and server logs. Retry retained deliveries once the service is available.',
);
expect(description).not.toContain('Review the local notification audit details');
expect(description).toContain('Recoverable retry attempts do not trigger this warning');
});
it('names the retention window and the test-send caveat so absence is not read as failure', () => {
expect(getAlertDestinationsDeliveryLogTitle()).toBe('Recent delivery activity');
const description = getAlertDestinationsDeliveryLogDescription(7, 30);
@@ -229,6 +229,7 @@ export function getAlertDestinationsDeliveryHealthDescription(
tls: 'Check certificate trust, hostname matching, and TLS settings.',
configuration: 'Review the enabled destination configuration and required fields.',
rejected: 'Check the destination endpoint and payload requirements.',
server_error: 'Check the destination service status and server logs. Retry retained deliveries once the service is available.',
unknown: 'Review the local notification audit details for the terminal error.',
};
let diagnostic = 'Check each enabled destination and send a test.';
@@ -329,6 +330,7 @@ export function getAlertDeliveryLogFailureClassLabel(failureClass: string) {
tls: 'TLS failure',
configuration: 'Configuration problem',
rejected: 'Rejected by destination',
server_error: 'Destination server error',
unknown: 'Unclassified failure',
};
return labels[failureClass] ?? labels.unknown;
@@ -0,0 +1,77 @@
// 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 { AlertDeliveryHealthCard } from '/src/features/alerts/AlertDeliveryHealthCard';
import { AlertDeliveryLogCard } from '/src/features/alerts/AlertDeliveryLogCard';
import '/src/index.css';
const noop = () => {};
render(() => <main class="p-4">
<AlertDeliveryHealthCard health={{status:'degraded',failed:1,deadLetter:0,failureClasses7d:{server_error:1},failureClassesAvailable:true}} unavailable={false} refreshing={false} onRefresh={noop}/>
<AlertDeliveryLogCard log={{entries:[{id:1,alertIds:['fixture-alert'],success:false,type:'webhook',outcome:'failed',failureClass:'server_error',timestamp:new Date().toISOString(),attempts:3}],completedRetentionDays:7,deadLetterRetentionDays:30}} unavailable={false} refreshing={false} onRefresh={noop} webhooks={[]}/>
</main>, document.getElementById('root'));
`;
const server = await createServer({
root,
configFile: false,
optimizeDeps: {
noDiscovery: true,
entries: [],
esbuildOptions: { target: "esnext" },
},
esbuild: { target: "esnext" },
plugins: [
solid(),
{
name: "ordering-fixture",
configureServer(s) {
s.middlewares.use((req, res, next) => {
if (req.url === "/qualification") {
res.setHeader("Content-Type", "text/html");
res.end(
'<div id="root"></div><script type="module" src="/ordering-fixture.tsx"></script>',
);
} else next();
});
},
resolveId(id) {
if (id === "/ordering-fixture.tsx") return id;
},
load(id) {
if (id === "/ordering-fixture.tsx") return fixture;
},
},
],
resolve: { alias: { "@": resolve(root, "src") } },
server: { host: "127.0.0.1", port: 5197, strictPort: true },
});
let browser;
try {
await server.listen();
browser = await chromium.launch({ headless: true });
mkdirSync("/tmp/pulse-server-error-copy", { recursive: true });
for (const width of [1440, 900, 390]) {
const page = await browser.newPage({ viewport: { width, height: 900 } });
const errors = [];
page.on('pageerror', error => errors.push(error.message));
await page.goto("http://127.0.0.1:5197/qualification");
await page.getByText('Destination server error', {exact:true}).waitFor();
assert.match(await page.getByRole('alert').innerText(), /Check the destination service status and server logs/);
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
assert.deepEqual(errors, []);
await page.screenshot({path: `/tmp/pulse-server-error-copy/${width}.png`, fullPage:true});
await page.close();
}
console.log(JSON.stringify({result:'passed',viewports:[1440,900,390],scope:'Real health/log cards, Chromium, scripted props; no installed delivery claim'}));
} finally {
await browser?.close();
await server.close();
}