diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md
index 37cb71d26..cc60f3d6d 100644
--- a/docs/release-control/v6/internal/subsystems/alerts.md
+++ b/docs/release-control/v6/internal/subsystems/alerts.md
@@ -15,6 +15,12 @@
## Purpose
+The alerts overview offers the existing delivery-status refresh control when
+health is unavailable, including after a successful retained-queue action whose
+follow-up health read fails. The warning remains until a verified healthy read;
+a successful queue action alone is not evidence of delivery health. Normal
+degraded summary presentation continues to omit refresh.
+
Confirmed canonical metric recovery publishes the clearing evaluation's value,
observation time, and resolved metric wording in the snapshot consumed by
recent-resolution reads and notification callbacks. It must not reuse the last
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 26a89168b..0828f81b6 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -20,6 +20,12 @@
## Purpose
+The alerts overview offers the existing delivery-status refresh control when
+health is unavailable, including after a successful retained-queue action whose
+follow-up health read fails. The warning remains until a verified healthy read;
+a successful queue action alone is not evidence of delivery health. Normal
+degraded summary presentation continues to omit refresh.
+
Proxmox backup presentation treats every manifestless PBS artifact as
non-recoverable. It renders the artifact as `Running` when current writer
visibility is absent or a matching writer is active, and as danger-tone
diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json
index c18e0751c..357d04b34 100644
--- a/frontend-modern/browser-verification.json
+++ b/frontend-modern/browser-verification.json
@@ -1,16 +1,16 @@
{
"version": 1,
- "base_sha": "2a833eccdfecf7248eed63eabf5b9184a9fa2390",
- "verified_at": "2026-09-05T23:57:09.414504Z",
+ "base_sha": "4cc53d10950f2effae720d251f92bf7d143d2093",
+ "verified_at": "2026-09-06T00:55:06.543758Z",
"result": "passed",
"changed_paths": [
- "frontend-modern/src/features/alerts/useNotificationDeliveryHealth.ts"
+ "frontend-modern/src/features/alerts/OverviewTab.tsx"
],
"content_sha256": {
- "frontend-modern/src/features/alerts/useNotificationDeliveryHealth.ts": "1eadf6df30b4f1130f868b8f139561ea60fdb87e1823deeeaaf2f8fbc699a00c"
+ "frontend-modern/src/features/alerts/OverviewTab.tsx": "8f7fdc04bd0546f86152c8bb392ff3e3fb755f1f0a1c28d1ee85da3e9fe23582"
},
"routes": [
- "/qualification (isolated caller/card fixture, not application routing)"
+ "/qualification (actual OverviewTab in isolated Solid Router fixture; not application shell)"
],
"viewports": [
{
@@ -23,12 +23,11 @@
}
],
"states": [
- "Real Chromium with current useAlertDestinationsTabState, useNotificationDeliveryHealth and AlertDeliveryHealthCard; scripted API promises and queue actions. No installed backend or provider receipt.",
- "Older healthy/newer degraded, older degraded/newer healthy, older error/newer healthy, older healthy/newer error. Warning presence and rendered state remain owned by newer request. Desktop and narrow screenshots inspected; this is not a full-shell or accessibility audit."
+ "Actual OverviewTab and delivery-health hook/card in Chromium, scripted API promises, empty active alerts. Not installed backend or recipient qualification.",
+ "Degraded health, successful Retry or Dismiss followed by unavailable health, pending manual refresh, verified healthy response. Desktop and narrow unavailable screenshots visually inspected; Refresh fits without clipping. Not a full accessibility or shell audit."
],
"interactions": [
- "pulse-heavy-run -- node scripts/check-delivery-health-ordering.mjs: 12 cases passed.",
- "Configuration Retry overlaps pending mount health. Resolve newer then older and compare rendered main text and alert presence.",
- "Retry retained deliveries and Dismiss retained failures each start post-action refresh while another read is pending. Older completion leaves loading true; latest healthy response clears warning. Confirmations and API responses are scripted."
+ "pulse-heavy-run -- node scripts/check-overview-health-refresh.mjs: four cases passed (two actions at two widths).",
+ "Retry and Dismiss each accepted once; failed health read preserves unavailable warning and offers Refresh; Refresh disabled while pending; healthy read removes warning without another queue action."
]
}
diff --git a/frontend-modern/src/features/alerts/AlertDeliveryHealthCard.test.tsx b/frontend-modern/src/features/alerts/AlertDeliveryHealthCard.test.tsx
index 9f3828c05..2651c01da 100644
--- a/frontend-modern/src/features/alerts/AlertDeliveryHealthCard.test.tsx
+++ b/frontend-modern/src/features/alerts/AlertDeliveryHealthCard.test.tsx
@@ -103,6 +103,26 @@ describe('AlertDeliveryHealthCard', () => {
expect(screen.getByRole('button', { name: 'Refresh delivery status' })).toBeDisabled();
});
+ it('allows unavailable summary health to be rechecked without a queue mutation', () => {
+ const onRefresh = vi.fn();
+ render(() => (
+
+ ));
+ expect(screen.getByRole('alert')).toHaveTextContent(
+ 'Notification delivery status is unavailable',
+ );
+ fireEvent.click(screen.getByRole('button', { name: 'Refresh delivery status' }));
+ expect(onRefresh).toHaveBeenCalledOnce();
+ expect(screen.getByRole('alert')).toBeTruthy();
+ });
+
it('keeps the overview treatment concise and points directly to delivery evidence', () => {
render(() => (
diff --git a/frontend-modern/src/features/alerts/OverviewTab.tsx b/frontend-modern/src/features/alerts/OverviewTab.tsx
index 219aeb25e..de8319f39 100644
--- a/frontend-modern/src/features/alerts/OverviewTab.tsx
+++ b/frontend-modern/src/features/alerts/OverviewTab.tsx
@@ -90,7 +90,7 @@ export function OverviewTab(props: {
onDismissFailures={() => void deliveryHealthState.dismissTerminalFailures()}
detailsHref="/alerts/notifications#notification-delivery-activity"
detailLevel="summary"
- showRefresh={false}
+ showRefresh={deliveryHealthState.deliveryHealthUnavailable()}
/>
diff --git a/frontend-modern/src/features/alerts/__tests__/OverviewTab.deliveryactions.test.tsx b/frontend-modern/src/features/alerts/__tests__/OverviewTab.deliveryactions.test.tsx
index 1851cf704..f7724875b 100644
--- a/frontend-modern/src/features/alerts/__tests__/OverviewTab.deliveryactions.test.tsx
+++ b/frontend-modern/src/features/alerts/__tests__/OverviewTab.deliveryactions.test.tsx
@@ -164,15 +164,58 @@ describe('OverviewTab delivery health actions', () => {
expect(screen.getByRole('button', { name: action.name })).not.toBeDisabled();
});
+ it(`keeps health visibly unknown after ${action.name} succeeds but refresh fails`, async () => {
+ getHealth
+ .mockResolvedValueOnce(degradedHealth())
+ .mockRejectedValueOnce(new Error('health unavailable'));
+ action.api.mockResolvedValue({ affected: 85 });
+ vi.spyOn(window, 'confirm').mockReturnValue(true);
+ render(() => );
+ fireEvent.click(await screen.findByRole('button', { name: action.name }));
+
+ const refresh = await screen.findByRole('button', { name: 'Refresh delivery status' });
+ expect(screen.getByRole('alert')).toBeTruthy();
+ expect(notificationStore.success).toHaveBeenCalledOnce();
+ expect(notificationStore.error).not.toHaveBeenCalled();
+
+ const healthy = degradedHealth();
+ healthy.overallHealthy = true;
+ healthy.queue = {
+ ...healthy.queue,
+ status: 'healthy',
+ healthy: true,
+ attentionRequired: 0,
+ deadLetter: 0,
+ };
+ getHealth.mockResolvedValueOnce(healthy);
+ await waitFor(() => expect(refresh).not.toBeDisabled());
+ fireEvent.click(refresh);
+ await waitFor(() => expect(screen.queryByRole('alert')).toBeNull());
+ expect(getHealth).toHaveBeenCalledTimes(3);
+ expect(action.api).toHaveBeenCalledOnce();
+ });
+
it(`keeps both actions disabled until ${action.name} and its health refresh finish`, async () => {
const healthy = degradedHealth();
healthy.overallHealthy = true;
- healthy.queue = { ...healthy.queue, status: 'healthy', healthy: true, attentionRequired: 0, deadLetter: 0 };
+ healthy.queue = {
+ ...healthy.queue,
+ status: 'healthy',
+ healthy: true,
+ attentionRequired: 0,
+ deadLetter: 0,
+ };
let completeAction!: (value: { affected: number }) => void;
let completeHealth!: (value: NotificationHealth) => void;
- action.api.mockReturnValue(new Promise(resolve => { completeAction = resolve; }));
+ action.api.mockReturnValue(
+ new Promise((resolve) => {
+ completeAction = resolve;
+ }),
+ );
getHealth.mockResolvedValueOnce(degradedHealth()).mockReturnValueOnce(
- new Promise(resolve => { completeHealth = resolve; }),
+ new Promise((resolve) => {
+ completeHealth = resolve;
+ }),
);
vi.spyOn(window, 'confirm').mockReturnValue(true);
render(() => );
@@ -197,5 +240,4 @@ describe('OverviewTab delivery health actions', () => {
expect(notificationStore.error).not.toHaveBeenCalled();
});
}
-
});
diff --git a/scripts/check-overview-health-refresh.mjs b/scripts/check-overview-health-refresh.mjs
new file mode 100644
index 000000000..494d67802
--- /dev/null
+++ b/scripts/check-overview-health-refresh.mjs
@@ -0,0 +1,98 @@
+// 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 { NotificationsAPI } from '/src/api/notifications';
+import { AlertsAPI } from '/src/api/alerts';
+import { OverviewTab } from '/src/features/alerts/OverviewTab';
+import '/src/index.css';
+const pending = [];
+NotificationsAPI.getHealth = () => new Promise((resolve, reject) => pending.push({resolve, reject}));
+AlertsAPI.getDeliveryDiagnoses = async () => [];
+window.actions = 0;
+NotificationsAPI.retryTerminalFailures = NotificationsAPI.dismissTerminalFailures = async () => { window.actions++; return {affected: 1}; };
+window.confirm = () => true;
+window.finish = (i, status) => status === 'error' ? pending[i].reject(new Error('scripted offline')) : pending[i].resolve({queue:{status, failed:0, deadLetter:status === 'healthy' ? 0 : 1, attentionRequired:status === 'healthy' ? 0 : 1}});
+window.count = () => pending.length;
+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: "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-overview-refresh', { recursive: true });
+ let cases = 0;
+ for (const width of [1440, 390]) {
+ for (const action of ['Retry retained deliveries', 'Dismiss retained failures']) {
+ const page = await browser.newPage({ viewport: { width, height: 900 } });
+ await page.goto('http://127.0.0.1:5197/qualification');
+ await page.waitForFunction(() => window.count?.() === 1);
+ await page.evaluate(() => window.finish(0, 'degraded'));
+ await page.getByRole('button', {name: action, exact: true}).click();
+ await page.waitForFunction(() => window.count() === 2);
+ await page.evaluate(() => window.finish(1, 'error'));
+ const refresh = page.getByRole('button', {name:'Refresh delivery status', exact:true});
+ await refresh.waitFor();
+ assert.match(await page.getByRole('alert').innerText(), /status is unavailable/);
+ await page.screenshot({path: `/tmp/pulse-overview-refresh/${width}-${cases}-unavailable.png`});
+ await refresh.click();
+ await page.waitForFunction(() => window.count() === 3);
+ assert.equal(await refresh.isDisabled(), true);
+ await page.evaluate(() => window.finish(2, 'healthy'));
+ await page.getByRole('alert').waitFor({state:'detached'});
+ assert.equal(await page.evaluate(() => window.actions), 1);
+ await page.screenshot({path: `/tmp/pulse-overview-refresh/${width}-${cases}-healthy.png`});
+ cases++;
+ await page.close();
+ }
+ }
+ console.log(`${cases} overview action/outage/refresh browser cases passed`);
+} finally {
+ await browser?.close();
+ await server.close();
+}