feat: add configurable notification dispatch retries (#1655)

* feat: add configurable notification dispatch retries

Allow each node to set 0-3 extra in-process delivery attempts with a fixed
one-second delay for routes, agents, and Test dispatch.

* fix: harden notification retry settings load/save and channel tests

Guard Delivery retries against failed and out-of-order same-node settings responses, and cover Slack/webhook retry classification alongside Discord.

* fix: clear Delivery retries saving state and correct screenshot alt

Separate save-request ownership from value-generation invalidation so a successful PATCH cannot leave Save retries stuck on Saving, reset saving on node switch, and align the Channels screenshot alt with the committed image.

* fix: surface invalid notification retry settings instead of false saved clamp

Align Channels GET handling with the backend strict 0-3 parser so stored values like 9 or 1.5 show as error needing repair, matching runtime fallback to 0 instead of displaying a clamped saved policy.
This commit is contained in:
Anso
2026-07-20 20:29:17 -04:00
committed by GitHub
parent 859839082c
commit 090a0d73ac
19 changed files with 1175 additions and 38 deletions
+11
View File
@@ -3,6 +3,7 @@ import { z } from 'zod';
import { DatabaseService } from '../services/DatabaseService';
import { authMiddleware } from '../middleware/auth';
import { requireAdmin, requirePaid } from '../middleware/tierGates';
import { parseNotificationDispatchRetries } from '../helpers/notificationDispatchRetries';
// Strict allowlist of keys readable and writable via the generic settings
// API. This is the single source of truth for what the endpoint exposes:
@@ -34,6 +35,7 @@ const ALLOWED_SETTING_KEYS = new Set([
'env_block_deploy_on_missing_required',
'auto_create_missing_external_networks',
'image_update_sidebar_indicators',
'notification_dispatch_retries',
]);
// Keys whose write requires a paid license, not just an admin role.
@@ -66,6 +68,15 @@ const SettingsPatchSchema = z.object({
env_block_deploy_on_missing_required: z.enum(['0', '1']),
auto_create_missing_external_networks: z.enum(['0', '1']),
image_update_sidebar_indicators: z.enum(['0', '1']),
// Strict: do not use bare z.coerce.number() (null/false/'' become 0; true becomes 1).
notification_dispatch_retries: z.unknown().superRefine((v, ctx) => {
if (parseNotificationDispatchRetries(v) === null) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Must be an integer from 0 to 3',
});
}
}).transform((v) => String(parseNotificationDispatchRetries(v)!)),
}).partial();
export const settingsRouter = Router();