feat(FN-4332): complete Step 4 — add notification failure controls
Fusion-Task-Id: FN-4332 Fusion-Task-Lineage: c920ac71-906b-458d-b496-1b60540b58f3
This commit is contained in:
@@ -404,6 +404,8 @@ export function SettingsModal({
|
||||
ntfyEnabled: false,
|
||||
ntfyTopic: undefined,
|
||||
ntfyAccessToken: undefined,
|
||||
failureNotificationMode: "sticky-only",
|
||||
failureNotificationDelayMs: 30000,
|
||||
webhookEnabled: false,
|
||||
webhookUrl: undefined,
|
||||
webhookFormat: "generic",
|
||||
@@ -5022,6 +5024,47 @@ export function SettingsModal({
|
||||
{renderScopeBanner()}
|
||||
<h4 className="settings-section-heading">Notifications</h4>
|
||||
|
||||
<div className="notification-provider-card">
|
||||
<div className="form-group">
|
||||
<label htmlFor="failureNotificationMode">Failure notification mode</label>
|
||||
<select
|
||||
id="failureNotificationMode"
|
||||
value={form.failureNotificationMode ?? "sticky-only"}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value as "sticky-only" | "all";
|
||||
setForm((f) => ({ ...f, failureNotificationMode: value }));
|
||||
}}
|
||||
>
|
||||
<option value="sticky-only">Sticky failures only (default)</option>
|
||||
<option value="all">All failures (legacy)</option>
|
||||
</select>
|
||||
<small>
|
||||
Sticky-only suppresses notifications for transient failures that the engine auto-recovers. Choose "All failures" for the legacy immediate-notification behavior.
|
||||
</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="failureNotificationDelayMs">Failure notification delay (ms)</label>
|
||||
<input
|
||||
id="failureNotificationDelayMs"
|
||||
type="number"
|
||||
min={0}
|
||||
step={1000}
|
||||
disabled={(form.failureNotificationMode ?? "sticky-only") === "all"}
|
||||
value={form.failureNotificationDelayMs ?? 30000}
|
||||
onChange={(e) => {
|
||||
const parsed = Number(e.target.value);
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
failureNotificationDelayMs: Number.isFinite(parsed) && parsed >= 0 ? parsed : 0,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<small>
|
||||
How long a failure must persist before a push notification is sent. 0 = notify immediately.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="notification-provider-card">
|
||||
<div className="notification-provider-header">
|
||||
<strong>ntfy</strong>
|
||||
|
||||
@@ -3245,6 +3245,34 @@ describe("SettingsModal", () => {
|
||||
expect(screen.getByText("Webhook")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders failure mode controls and persists updated values", async () => {
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
await openNotificationsSection();
|
||||
|
||||
const modeSelect = screen.getByLabelText("Failure notification mode") as HTMLSelectElement;
|
||||
const delayInput = screen.getByLabelText("Failure notification delay (ms)") as HTMLInputElement;
|
||||
|
||||
expect(modeSelect.value).toBe("sticky-only");
|
||||
expect(delayInput.value).toBe("30000");
|
||||
|
||||
await userEvent.selectOptions(modeSelect, "all");
|
||||
expect(delayInput).toBeDisabled();
|
||||
await userEvent.selectOptions(modeSelect, "sticky-only");
|
||||
await userEvent.clear(delayInput);
|
||||
await userEvent.type(delayInput, "45000");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateGlobalSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
failureNotificationMode: "sticky-only",
|
||||
failureNotificationDelayMs: 45000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows ntfy fields when ntfy provider is enabled", async () => {
|
||||
mockFetchSettings.mockResolvedValueOnce({ ...defaultSettings, ntfyEnabled: true, ntfyTopic: "test-topic" });
|
||||
renderModal();
|
||||
|
||||
Reference in New Issue
Block a user