diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index 38fbadbed..5b12dddb7 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -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. diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 96140d5a1..6f93fc4c5 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -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" } diff --git a/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts b/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts index 59c25c312..441e4cf2e 100644 --- a/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts @@ -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); diff --git a/frontend-modern/src/utils/alertDestinationsPresentation.ts b/frontend-modern/src/utils/alertDestinationsPresentation.ts index e4721617e..6e4d790bb 100644 --- a/frontend-modern/src/utils/alertDestinationsPresentation.ts +++ b/frontend-modern/src/utils/alertDestinationsPresentation.ts @@ -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; diff --git a/scripts/check-delivery-server-error-copy.mjs b/scripts/check-delivery-server-error-copy.mjs new file mode 100644 index 000000000..135c83dc1 --- /dev/null +++ b/scripts/check-delivery-server-error-copy.mjs @@ -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(() =>
+ + +
, 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( + '
', + ); + } 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(); +}