mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
fix(alerts): preserve warning-level notification preferences
Retain warning severity through email API save/reload and webhook editing instead of coercing it to all. Add adapter round-trip and webhook edit/save regressions for issue #2069 with alerts, API and notifications contracts. Browser verification covers production components at desktop and narrow widths using synthetic settings; backend filtering is unchanged. Change-source: pulse-maintainer
This commit is contained in:
@@ -2936,3 +2936,11 @@ retention, alert freshness or the existing persistence durability contract.
|
||||
starting database workers, including restart loading and Unix hardening. The
|
||||
active mirror's unchanged fast path alone does not cover this companion file.
|
||||
This is a narrow write contributor, not aggregate installed-write acceptance.
|
||||
|
||||
### Warning-level destination preference persistence
|
||||
|
||||
Email API decoding and encoding, plus webhook edit/create and list presentation,
|
||||
preserve all three supported minimum-severity values: all, warning, critical.
|
||||
Unknown values still fall back to all. The email adapter round-trip matrix and
|
||||
webhook edit/save regression cover issue #2069 without altering delivery filters
|
||||
or sending provider notifications. Component proof is not installed delivery.
|
||||
|
||||
@@ -10828,3 +10828,11 @@ gates retain the explicit credential's normal validation and scopes. In
|
||||
particular, runner activation and self-revocation continue to require the exact
|
||||
credential-bound host and tenant, including in the managed development runtime.
|
||||
Invalid explicit credentials cannot fall back to the convenience bypass.
|
||||
|
||||
### Notification severity round trips
|
||||
|
||||
Email management payload mapping preserves all, warning and critical in both
|
||||
GET decoding and PUT encoding. Unknown inputs retain the all fallback. This
|
||||
repairs a dropped supported value without introducing a new wire enum or changing
|
||||
backend routing. The notification adapter save/reload matrix verifies each
|
||||
supported severity in frontend-modern/src/api/__tests__/notifications.test.ts.
|
||||
|
||||
@@ -910,3 +910,11 @@ destination enablement. Resolution independently cancels obsolete firing work.
|
||||
initial-target edit with an in-memory SMTP acceptance, and prevents a previously
|
||||
fetched pending item from sending after cancellation. This does not establish
|
||||
recipient inbox timing or recall mail already accepted by a provider.
|
||||
|
||||
### Warning-level destination controls
|
||||
|
||||
Email settings and webhook edit/create/list controls retain warning as distinct
|
||||
from all and critical. A warning selection must not become all through frontend
|
||||
normalization. Existing backend severity admission and delivery queues remain
|
||||
unchanged. The notification API adapter round trips and WebhookConfig edit/save
|
||||
regression establish frontend control preservation, not provider delivery.
|
||||
|
||||
@@ -1,83 +1,44 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "5a4163ff83890005142ed81b025b8dea2a755b56",
|
||||
"verified_at": "2026-09-12T16:22:19.408045Z",
|
||||
"base_sha": "ce1e99ba4e37b735d22158ac74a93afa26d7611f",
|
||||
"verified_at": "2026-09-14T21:33:45.967568Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/components/Workloads/WorkloadsSurface.tsx",
|
||||
"frontend-modern/src/components/Workloads/useWorkloadFilterOptions.ts",
|
||||
"frontend-modern/src/components/Workloads/useWorkloadRouteState.ts",
|
||||
"frontend-modern/src/components/Workloads/useWorkloadsState.ts",
|
||||
"frontend-modern/src/components/Workloads/workloadRouteModel.ts",
|
||||
"frontend-modern/src/components/Workloads/workloadTopology.ts",
|
||||
"frontend-modern/src/components/shared/contextualFocus.ts",
|
||||
"frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx"
|
||||
"frontend-modern/src/api/notifications.ts",
|
||||
"frontend-modern/src/components/Alerts/WebhookConfigList.tsx",
|
||||
"frontend-modern/src/components/Alerts/useWebhookConfigState.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/components/Workloads/WorkloadsSurface.tsx": "8f0befdd56d332c9fb9e74319b8a7cb3db6fcb5518cd6dbc560f0710d83bd471",
|
||||
"frontend-modern/src/components/Workloads/useWorkloadFilterOptions.ts": "d4e44b1410578876832ae8ba205b5e212154b8b940baa7f42bf72c4b5611ccde",
|
||||
"frontend-modern/src/components/Workloads/useWorkloadRouteState.ts": "b669aed8d88287230a72d98d6916ed77c51455ea0c68fbda3d0623a66859944f",
|
||||
"frontend-modern/src/components/Workloads/useWorkloadsState.ts": "1d515d140099a05219f97773b988d065bc5784d95910626f0e797ff3acd1e283",
|
||||
"frontend-modern/src/components/Workloads/workloadRouteModel.ts": "fb8d8d45ee9acce9359efefb2778a7a246946c84da25f502cbddb398d0ff02a6",
|
||||
"frontend-modern/src/components/Workloads/workloadTopology.ts": "0917dfb1d9e7b1d65e3ff3afebc9a725b68d40188177fe7181547aebc9b1c3c7",
|
||||
"frontend-modern/src/components/shared/contextualFocus.ts": "2c83a1ab5ca16c45d0c8a90bbdc0a7d75aedb1c1ec46b5513e31fbc26dfcab2e",
|
||||
"frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx": "76a2ccdbfa460492f4eb3b6cae91bda72d15915c6b172056c987e18a11116c94",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx": "33c6804b64cbacc0e770aabdd162a3e5121e0db5947d91fde95297c621772459",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "d4c7072f66a8a8ed50d920a18982ffef2ffca1fd972daa6471d3c0fe5857ef24"
|
||||
"frontend-modern/src/api/notifications.ts": "6eb5080070913baa025b07ffef1c38f3297ddfce8095ff71b6297b7e2cf09892",
|
||||
"frontend-modern/src/components/Alerts/WebhookConfigList.tsx": "0830ab5c9f7e0173dbdf3e92ed0e0c9cbf0a34942edb58001a3220d43b39d6c3",
|
||||
"frontend-modern/src/components/Alerts/useWebhookConfigState.ts": "e79778f3e0b85536c672509210a81d258f8f8e483d09f5ef295118d428cf7a70"
|
||||
},
|
||||
"routes": [
|
||||
"/proxmox/overview"
|
||||
"/browser-tests/severity.html"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 700
|
||||
},
|
||||
{
|
||||
"width": 1024,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Default inventory and inert node hover",
|
||||
"Selected and cleared node with matching guest count",
|
||||
"Empty node and type-filtered empty result",
|
||||
"Node and guest details expanded independently",
|
||||
"Restored node selection after reload",
|
||||
"Mobile filters expanded and collapsed",
|
||||
"Node name default and hover without link decoration",
|
||||
"Keyboard focus ring without underline",
|
||||
"Selected and unselected guest scope",
|
||||
"Separate desktop external website link"
|
||||
"Existing warning email and webhook",
|
||||
"All, critical and warning severity saved and reloaded",
|
||||
"Cancelled webhook edit",
|
||||
"New warning webhook persisted"
|
||||
],
|
||||
"interactions": [
|
||||
"Click node name, count and plain row cell to filter guests",
|
||||
"Activate the node name with Enter and Space, and select with touch",
|
||||
"Reveal and focus the guest heading when it starts below the viewport",
|
||||
"Expand and collapse node details with the chevron without changing guest scope",
|
||||
"Keep external links independent of row selection",
|
||||
"Switch nodes and activate the selected node again to clear its scope with pointer, keyboard and touch",
|
||||
"Preserve search, status and type filters while selecting and clearing nodes",
|
||||
"Open guest details and dismiss with Escape while retaining node scope",
|
||||
"Inspect desktop, intermediate and mobile pixels, including touch controls and empty results",
|
||||
"Assert the selected node keeps its screen position and keyboard focus when deselected, including with a long open details panel",
|
||||
"Select and switch minipc, delly and delly2 with an already visible guest heading and assert unchanged node screen position at all four viewports",
|
||||
"Hover node name and inspect computed decoration and actual pixels",
|
||||
"Tab and Shift+Tab to node name and inspect visible keyboard focus ring",
|
||||
"Enter selects guests and Space clears node scope",
|
||||
"Touch selects and clears the node on phone",
|
||||
"Verify external website icon remains an independent link"
|
||||
"Change production email severity selector, save through NotificationsAPI to synthetic backend, reload page and assert each supported value",
|
||||
"Edit production webhook, save and reload each supported severity",
|
||||
"Cancel changed webhook severity and verify original warning preserved",
|
||||
"Create warning webhook and reload synthetic persisted store",
|
||||
"Inspect desktop edit and narrow saved screenshots"
|
||||
],
|
||||
"command": "Playwright scripts check.cjs, adjacent.cjs, reveal.cjs, deselect.cjs and selection-scroll.cjs against the current local Vite server",
|
||||
"notes": "Live inventory. Final source hashes match the browser-verified implementation. Screenshots and interaction scripts retained locally in the proxmox-node-row-review receipt directory. Rebased onto current main with byte-identical verified runtime files. Final presentation follow-up removes the node-name hover underline. The affected hover, focus, selection and touch matrix passed at1440x1000 and390x844, with other runtime content unchanged."
|
||||
"command": "pulse-worker-browser severity-browser.cjs",
|
||||
"notes": "Production-component fixture built with Vite esnext and project CSS. Synthetic email HTTP responses and webhook localStorage persistence; no live backend, notification delivery or whole-application navigation proof. Runtime source hashes match the verified implementation. Desktop edit and narrow saved screenshots inspected."
|
||||
}
|
||||
|
||||
@@ -424,3 +424,20 @@ describe('NotificationsAPI', () => {
|
||||
expect(result.status).toBe('success');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('email minimum severity save/reload (#2069)', () => {
|
||||
it.each(['all', 'warning', 'critical'] as const)('preserves %s through the API adapter', async (minimumSeverity) => {
|
||||
const mock = vi.mocked(apiFetchJSON);
|
||||
mock.mockReset();
|
||||
mock.mockResolvedValueOnce({ minimumSeverity });
|
||||
const loaded = await NotificationsAPI.getEmailConfig();
|
||||
expect(loaded.minimumSeverity).toBe(minimumSeverity);
|
||||
mock.mockResolvedValueOnce({ success: true });
|
||||
await NotificationsAPI.updateEmailConfig(loaded);
|
||||
const saved = JSON.parse(String(mock.mock.calls[1][1]?.body));
|
||||
expect(saved.minimumSeverity).toBe(minimumSeverity);
|
||||
mock.mockResolvedValueOnce(saved);
|
||||
expect((await NotificationsAPI.getEmailConfig()).minimumSeverity).toBe(minimumSeverity);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -437,7 +437,9 @@ export class NotificationsAPI {
|
||||
if (typeof backendConfig.tagFilterMode === 'string') {
|
||||
config.tagFilterMode = backendConfig.tagFilterMode === 'any' ? 'any' : 'all';
|
||||
}
|
||||
config.minimumSeverity = backendConfig.minimumSeverity === 'critical' ? 'critical' : 'all';
|
||||
config.minimumSeverity = backendConfig.minimumSeverity === 'critical' || backendConfig.minimumSeverity === 'warning'
|
||||
? backendConfig.minimumSeverity
|
||||
: 'all';
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -467,7 +469,9 @@ export class NotificationsAPI {
|
||||
backendConfig.tagFilterMode = config.tagFilterMode === 'any' ? 'any' : 'all';
|
||||
}
|
||||
if (config.minimumSeverity !== undefined) {
|
||||
backendConfig.minimumSeverity = config.minimumSeverity === 'critical' ? 'critical' : 'all';
|
||||
backendConfig.minimumSeverity = config.minimumSeverity === 'critical' || config.minimumSeverity === 'warning'
|
||||
? config.minimumSeverity
|
||||
: 'all';
|
||||
}
|
||||
|
||||
return apiFetchJSON(`${this.baseUrl}/email`, {
|
||||
|
||||
@@ -711,6 +711,17 @@ describe('WebhookConfig', () => {
|
||||
expect(urlInput.value).toBe('https://edit.example.com');
|
||||
});
|
||||
|
||||
it('preserves warning severity when editing and saving a webhook', () => {
|
||||
const webhook = makeWebhook({ minimumSeverity: 'warning' });
|
||||
render(() => (
|
||||
<WebhookConfig webhooks={[webhook]} onAdd={onAddMock} onUpdate={onUpdateMock}
|
||||
onDelete={onDeleteMock} onTest={onTestMock} />
|
||||
));
|
||||
fireEvent.click(screen.getByText('Edit'));
|
||||
fireEvent.click(screen.getByText('Update Webhook'));
|
||||
expect(onUpdateMock).toHaveBeenCalledWith(expect.objectContaining({ minimumSeverity: 'warning' }));
|
||||
});
|
||||
|
||||
it('calls onUpdate with updated data when saving an edited webhook', () => {
|
||||
const webhook = makeWebhook({
|
||||
id: 'wh-edit',
|
||||
|
||||
@@ -78,7 +78,9 @@ export function WebhookConfigList(props: WebhookConfigListProps) {
|
||||
</span>
|
||||
<span class="rounded bg-surface-alt px-2 py-0.5 text-base-content">
|
||||
{getAlertDestinationSeverityLabel(
|
||||
webhook.minimumSeverity === 'critical' ? 'critical' : 'all',
|
||||
webhook.minimumSeverity === 'critical' || webhook.minimumSeverity === 'warning'
|
||||
? webhook.minimumSeverity
|
||||
: 'all',
|
||||
)}
|
||||
</span>
|
||||
<For each={webhook.tagFilter ?? []}>
|
||||
|
||||
@@ -185,7 +185,9 @@ export function useWebhookConfigState(props: WebhookConfigProps): WebhookConfigS
|
||||
mention: webhook.mention || '',
|
||||
tagFilter: webhook.tagFilter ?? [],
|
||||
tagFilterMode: webhook.tagFilterMode === 'any' ? 'any' : 'all',
|
||||
minimumSeverity: webhook.minimumSeverity === 'critical' ? 'critical' : 'all',
|
||||
minimumSeverity: webhook.minimumSeverity === 'critical' || webhook.minimumSeverity === 'warning'
|
||||
? webhook.minimumSeverity
|
||||
: 'all',
|
||||
});
|
||||
|
||||
const headers = webhook.headers || {};
|
||||
@@ -271,7 +273,9 @@ export function useWebhookConfigState(props: WebhookConfigProps): WebhookConfigS
|
||||
mention: data.mention,
|
||||
tagFilter: data.tagFilter ?? [],
|
||||
tagFilterMode: data.tagFilterMode === 'any' ? 'any' : 'all',
|
||||
minimumSeverity: data.minimumSeverity === 'critical' ? 'critical' : 'all',
|
||||
minimumSeverity: data.minimumSeverity === 'critical' || data.minimumSeverity === 'warning'
|
||||
? data.minimumSeverity
|
||||
: 'all',
|
||||
});
|
||||
resetForm();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user