From 3fd5e1a50c8da39c53e8bf44ef7bb1910f175fdf Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 22:52:10 +0100 Subject: [PATCH] Repair held-notification governance proof Bind the combined delivery-attempt and held-event activity surface to the alerts, API, and frontend contracts. Add recognized state and presentation proofs and record desktop, refresh, and narrow-browser verification for the current feature commit. Change-source: pulse-maintainer --- .../v6/internal/subsystems/alerts.md | 10 ++++- .../v6/internal/subsystems/api-contracts.md | 7 +++- .../subsystems/frontend-primitives.md | 9 +++++ frontend-modern/browser-verification.json | 37 +++++++------------ .../useAlertDestinationsTabState.test.tsx | 32 ++++++++++++++++ .../alertDestinationsPresentation.test.ts | 1 + 6 files changed, 70 insertions(+), 26 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index b995e9372b..b78b426aba 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -1863,7 +1863,15 @@ newest-first record of real alert delivery attempts with plain-language outcome labels, destination names resolved from the loaded webhook configs, failure classes, and secret-redacted error text. The card names its retention window and says that test sends skip the queue, and an unreadable log renders -as unavailable rather than empty. Test-send results that report +as unavailable rather than empty. The same card interleaves alert-owned +`notification_suppressed` and `notification_deferred` evidence from +`GET /api/alerts/events` with those attempts, ordered by the recorded event or +attempt time. Held rows name the affected resource, alert type, and +plain-language policy reason even when no delivery attempt exists. The event +read is independent and fail-soft: an unavailable event log removes held rows +without delaying, hiding, or marking the notifications-owned attempt log as +unavailable. Refreshing delivery evidence starts both reads from one operator +action while preserving that failure isolation. Test-send results that report `deliveryPaused` surface as a warning toast, never plain success, across the email, Apprise, and webhook test actions in `useAlertDestinationsTabState` and `useAlertWebhookDestinationsState`. Delivery evidence remains diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index cb8b88202e..e705e807b8 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -4454,7 +4454,12 @@ event store returns `[]` rather than `null`. The event payload is evidence of recorded lifecycle and notification-decision seams only. It must not dispatch, acknowledge, resolve, or otherwise mutate alert or notification state, and the current absence of a fired lifecycle event must not be inferred as a firing -that never happened. +that never happened. `frontend-modern/src/api/alerts.ts` owns the typed client +projection and shared `arrayOrEmpty` normalization. The Destinations surface +requests only the two held-notification event types over the same seven-day +window as its delivery-attempt log, with an explicit bounded limit. That event +request runs independently of `GET /api/notifications/delivery-log`; failure +of either API must not recast the other response as empty or unavailable. The generated PVE setup-script temperature wrapper is part of the API contract for legacy SSH sensor collection. Discovery uses non-opening `smartctl --scan` diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index eaec60a7a8..a9a68437b8 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -434,6 +434,15 @@ bulk diagnosis snapshot per overview refresh, wraps beside started-at and threshold metadata at narrow widths, and does not create a second card or page-level banner. Acknowledged cards omit the redundant row, and unavailable diagnosis data leaves the existing alert card intact. +The Destinations delivery-activity surface follows the same ownership split: +alerts own the `AlertDeliveryLogCard` composition inside the shared `Card`, +button, badge, list, and responsive wrapping primitives. Its single +newest-first list may mix notification delivery attempts with alert-policy held +events, but each row retains its owning outcome vocabulary and evidence +tooltip. At narrow widths, resource, reason, and relative-time fields wrap or +truncate inside the card without creating page-level horizontal overflow, and +the shared refresh action remains independently usable while the bounded list +scrolls. The legacy pricing handoff page may also route its visible redirect title and manual-link copy through the catalog, but `Pulse Account`, route paths, feature keys, query parameters, public URLs, and purchase-return state remain diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index a83a147916..74ef1843bb 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,23 +1,11 @@ { "version": 1, - "base_sha": "515aa319e8c61fe9658f5ecfec417291df30a1da", - "verified_at": "2026-08-26T20:18:28Z", + "base_sha": "954ea1050aa121558b34d364ff484d8a4d482421", + "verified_at": "2026-08-26T21:47:28Z", "result": "passed", - "changed_paths": [ - "frontend-modern/src/api/alerts.ts", - "frontend-modern/src/features/alerts/AlertOverviewAlertCard.tsx", - "frontend-modern/src/features/alerts/deliveryDiagnosisPresentation.ts", - "frontend-modern/src/features/alerts/useAlertOverviewState.ts", - "frontend-modern/src/types/api.ts" - ], - "content_sha256": { - "frontend-modern/src/api/alerts.ts": "315ff7f3e2424e736a17a0a440511eec254ed5f1aba5426249c72318e0aac24a", - "frontend-modern/src/features/alerts/AlertOverviewAlertCard.tsx": "a16ca92470d7833003d3e6812bc464e51e5c34e4aa600ea2ad1cfda9d32e9635", - "frontend-modern/src/features/alerts/deliveryDiagnosisPresentation.ts": "ce55e5c8c2a6474768f62fadba70bd9c00f65d87985931b4eb23166f60c4ca3f", - "frontend-modern/src/features/alerts/useAlertOverviewState.ts": "8c923babd58f4942803f04a891f5c1a9a80aa7770d367faa01b9036edfa0015b", - "frontend-modern/src/types/api.ts": "076917acef66323e0914db6f56ab54261cd74e10cbb435a0d1d592bb14062c94" - }, - "routes": ["/alerts/overview"], + "changed_paths": [], + "content_sha256": {}, + "routes": ["/alerts/notifications"], "viewports": [ { "width": 1440, @@ -29,14 +17,15 @@ } ], "states": [ - "mock-mode Alerts Overview loaded with active critical and warning alerts while external notification delivery was not active", - "bulk delivery diagnosis rendered Notification delivery not turned on beside active-alert timing metadata", - "the same active-alert delivery state rendered in the narrow responsive card layout" + "mock-mode Alerts Notifications loaded with notification delivery paused and an empty delivery-attempt log", + "the append-only alert event log supplied 100 bounded held-notification rows with resource, alert type, policy reason, and recorded message evidence", + "the same held-only activity state rendered in the narrow responsive card layout" ], "interactions": [ - "loaded the current managed development build at /alerts/overview and confirmed the active-alert list populated from the bulk diagnosis read", - "confirmed delivery status appeared inline with Started metadata while Acknowledge, Timeline, monitoring-policy, and investigation controls remained usable on desktop", - "resized from 1440x900 to 760x900 and confirmed the delivery status and adjacent metadata wrapped without clipping or overlap", - "confirmed the browser console contained no errors across the desktop and narrow checks" + "loaded the current managed mock build at /alerts/notifications and confirmed Recent delivery activity rendered Held rows even though no delivery attempt existed", + "confirmed every inspected row exposed its full recorded suppression message while its visible badge, resource/type label, plain-language reason, and relative time remained distinct", + "used Refresh delivery status and confirmed the independently loaded held rows updated and remained newest first", + "resized from 1440x900 to 760x900 and confirmed the activity header, refresh action, and held rows remained usable without clipping or page-level horizontal overflow", + "confirmed the browser console contained no errors across the desktop, refresh, and narrow checks" ] } diff --git a/frontend-modern/src/features/alerts/__tests__/useAlertDestinationsTabState.test.tsx b/frontend-modern/src/features/alerts/__tests__/useAlertDestinationsTabState.test.tsx index 485836c4f4..34bc2ecf47 100644 --- a/frontend-modern/src/features/alerts/__tests__/useAlertDestinationsTabState.test.tsx +++ b/frontend-modern/src/features/alerts/__tests__/useAlertDestinationsTabState.test.tsx @@ -2,6 +2,7 @@ import { renderHook, waitFor } from '@solidjs/testing-library'; import { createSignal } from 'solid-js'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { AlertsAPI } from '@/api/alerts'; import { NotificationsAPI } from '@/api/notifications'; import { notificationStore } from '@/stores/notifications'; import { showErrorWithDetail } from '@/utils/toast'; @@ -24,6 +25,12 @@ vi.mock('@/api/notifications', () => ({ }, })); +vi.mock('@/api/alerts', () => ({ + AlertsAPI: { + getEvents: vi.fn(), + }, +})); + vi.mock('@/stores/notifications', () => ({ notificationStore: { error: vi.fn(), @@ -75,6 +82,8 @@ const buildAppriseConfig = (): UIAppriseConfig => ({ describe('useAlertDestinationsTabState', () => { beforeEach(() => { + vi.mocked(AlertsAPI.getEvents).mockReset(); + vi.mocked(AlertsAPI.getEvents).mockResolvedValue([]); vi.mocked(NotificationsAPI.createWebhook).mockReset(); vi.mocked(NotificationsAPI.deleteWebhook).mockReset(); vi.mocked(NotificationsAPI.getDeliveryLog).mockReset(); @@ -154,6 +163,18 @@ describe('useAlertDestinationsTabState', () => { ], windowDays: 7, }); + vi.mocked(AlertsAPI.getEvents).mockResolvedValue([ + { + id: 41, + occurredAt: '2026-08-20T12:01:00Z', + type: 'notification_suppressed', + alertId: 'disk-critical-2', + resourceName: 'backup-pool', + alertType: 'usage', + reason: 'notifications_inactive', + message: 'Notification suppressed: alert delivery is not turned on.', + }, + ]); vi.mocked(NotificationsAPI.testNotification).mockResolvedValue({ status: 'success' } as never); vi.mocked(NotificationsAPI.testWebhook).mockResolvedValue({ success: true } as never); @@ -172,8 +193,18 @@ describe('useAlertDestinationsTabState', () => { await waitFor(() => expect(NotificationsAPI.getWebhooks).toHaveBeenCalledTimes(1)); await waitFor(() => expect(NotificationsAPI.getHealth).toHaveBeenCalledTimes(1)); await waitFor(() => expect(NotificationsAPI.getDeliveryLog).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(AlertsAPI.getEvents).toHaveBeenCalledTimes(1)); + expect(AlertsAPI.getEvents).toHaveBeenCalledWith( + expect.objectContaining({ + types: ['notification_suppressed', 'notification_deferred'], + limit: 100, + }), + ); expect(result.deliveryHealth()?.queue.status).toBe('healthy'); expect(result.deliveryLog()?.entries).toHaveLength(1); + expect(result.heldEvents()).toEqual([ + expect.objectContaining({ reason: 'notifications_inactive', resourceName: 'backup-pool' }), + ]); expect(result.deliveryLogUnavailable()).toBe(false); expect(result.webhooks()).toEqual([ expect.objectContaining({ id: 'hook-1', service: 'generic' }), @@ -209,6 +240,7 @@ describe('useAlertDestinationsTabState', () => { await waitFor(() => expect(NotificationsAPI.getWebhooks).toHaveBeenCalledTimes(2)); await waitFor(() => expect(NotificationsAPI.getHealth).toHaveBeenCalledTimes(2)); await waitFor(() => expect(NotificationsAPI.getDeliveryLog).toHaveBeenCalledTimes(2)); + await waitFor(() => expect(AlertsAPI.getEvents).toHaveBeenCalledTimes(2)); expect(notificationStore.success).toHaveBeenCalledTimes(2); expect(notificationStore.warning).not.toHaveBeenCalled(); expect(showErrorWithDetail).not.toHaveBeenCalled(); diff --git a/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts b/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts index 2e84db3158..5719b0a9ac 100644 --- a/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts @@ -205,6 +205,7 @@ describe('alert destinations delivery log copy', () => { expect(getAlertDestinationsDeliveryLogTitle()).toBe('Recent delivery activity'); const description = getAlertDestinationsDeliveryLogDescription(7); expect(description).toContain('last 7 days'); + expect(description).toContain('held notifications'); expect(description).toContain('Test sends skip the queue'); expect(getAlertDestinationsDeliveryLogEmpty()).toContain('No alert deliveries were attempted'); // An unreadable log must never present itself as an empty one.