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,
|
ntfyEnabled: false,
|
||||||
ntfyTopic: undefined,
|
ntfyTopic: undefined,
|
||||||
ntfyAccessToken: undefined,
|
ntfyAccessToken: undefined,
|
||||||
|
failureNotificationMode: "sticky-only",
|
||||||
|
failureNotificationDelayMs: 30000,
|
||||||
webhookEnabled: false,
|
webhookEnabled: false,
|
||||||
webhookUrl: undefined,
|
webhookUrl: undefined,
|
||||||
webhookFormat: "generic",
|
webhookFormat: "generic",
|
||||||
@@ -5022,6 +5024,47 @@ export function SettingsModal({
|
|||||||
{renderScopeBanner()}
|
{renderScopeBanner()}
|
||||||
<h4 className="settings-section-heading">Notifications</h4>
|
<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-card">
|
||||||
<div className="notification-provider-header">
|
<div className="notification-provider-header">
|
||||||
<strong>ntfy</strong>
|
<strong>ntfy</strong>
|
||||||
|
|||||||
@@ -3245,6 +3245,34 @@ describe("SettingsModal", () => {
|
|||||||
expect(screen.getByText("Webhook")).toBeInTheDocument();
|
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 () => {
|
it("shows ntfy fields when ntfy provider is enabled", async () => {
|
||||||
mockFetchSettings.mockResolvedValueOnce({ ...defaultSettings, ntfyEnabled: true, ntfyTopic: "test-topic" });
|
mockFetchSettings.mockResolvedValueOnce({ ...defaultSettings, ntfyEnabled: true, ntfyTopic: "test-topic" });
|
||||||
renderModal();
|
renderModal();
|
||||||
|
|||||||
Reference in New Issue
Block a user