diff --git a/apps/api/src/routes/monitors.ts b/apps/api/src/routes/monitors.ts index 84314ec..1cc4539 100644 --- a/apps/api/src/routes/monitors.ts +++ b/apps/api/src/routes/monitors.ts @@ -15,7 +15,7 @@ const MonitorBody = t.Object({ max_retries: t.Optional(t.Number({ minimum: 0, maximum: 10, default: 0, description: "Retry a failing check up to N times before declaring DOWN" })), retry_interval_s: t.Optional(t.Number({ minimum: 1, maximum: 600, default: 30, description: "Seconds between retries" })), resend_interval: t.Optional(t.Number({ minimum: 0, maximum: 1000, default: 0, description: "Re-alert every Nth consecutive down beat. 0 = never resend." })), - cert_alert_days: t.Optional(t.Number({ minimum: 0, maximum: 365, default: 14, description: "Alert when TLS cert is within N days of expiry. 0 disables." })), + cert_alert_days: t.Optional(t.Number({ minimum: 0, maximum: 365, default: 0, description: "Alert when TLS cert is within N days of expiry. 0 disables (default)." })), query: t.Optional(t.Any({ description: "PingQL query — filter conditions for up/down" })), regions: t.Optional(t.Array(t.String(), { description: "Regions to run checks from. Empty array = all regions." })), channel_ids: t.Optional(t.Array(t.String(), { description: "Notification channel IDs to attach to this monitor." })), @@ -58,6 +58,12 @@ export const monitors = new Elysia({ prefix: "/monitors" }) return { error: `Minimum interval for ${plan} plan is ${limits.minIntervalS}s` }; } + const retryGap = body.retry_interval_s ?? Math.max(30, limits.minIntervalS); + if (retryGap < limits.minIntervalS) { + set.status = 400; + return { error: `Retry interval for ${plan} plan must be at least ${limits.minIntervalS}s` }; + } + const regions = body.regions ?? []; if (regions.length > limits.maxRegions) { set.status = 400; @@ -76,9 +82,9 @@ export const monitors = new Elysia({ prefix: "/monitors" }) ${body.timeout_ms ?? 10000}, ${interval}, ${body.max_retries ?? 0}, - ${body.retry_interval_s ?? 30}, + ${retryGap}, ${body.resend_interval ?? 0}, - ${body.cert_alert_days ?? 14}, + ${body.cert_alert_days ?? 0}, ${body.query ? sql.json(body.query) : null}, ${sql.array(regions)} ) @@ -112,6 +118,11 @@ export const monitors = new Elysia({ prefix: "/monitors" }) return { error: `Minimum interval for ${plan} plan is ${limits.minIntervalS}s` }; } + if (body.retry_interval_s != null && body.retry_interval_s < limits.minIntervalS) { + set.status = 400; + return { error: `Retry interval for ${plan} plan must be at least ${limits.minIntervalS}s` }; + } + if (body.regions && body.regions.length > limits.maxRegions) { set.status = 400; return { error: `Free plan allows ${limits.maxRegions} region per monitor. Upgrade to use multi-region.` }; diff --git a/apps/shared/db.ts b/apps/shared/db.ts index 6ce9c52..cdfbee4 100644 --- a/apps/shared/db.ts +++ b/apps/shared/db.ts @@ -72,7 +72,8 @@ export async function migrate(sql: any) { await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS max_retries INTEGER NOT NULL DEFAULT 0`; await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS retry_interval_s INTEGER NOT NULL DEFAULT 30`; await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS resend_interval INTEGER NOT NULL DEFAULT 0`; - await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS cert_alert_days INTEGER NOT NULL DEFAULT 14`; + await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS cert_alert_days INTEGER NOT NULL DEFAULT 0`; + await sql`ALTER TABLE monitors ALTER COLUMN cert_alert_days SET DEFAULT 0`; await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS important BOOLEAN NOT NULL DEFAULT false`; // Per-region transition state. region='' for unspecified/single-region monitors. diff --git a/apps/web/src/routes/dashboard.ts b/apps/web/src/routes/dashboard.ts index 19c71a7..bbc6640 100644 --- a/apps/web/src/routes/dashboard.ts +++ b/apps/web/src/routes/dashboard.ts @@ -525,7 +525,7 @@ export const dashboard = new Elysia() max_retries: Number(b.max_retries) || 0, retry_interval_s: Number(b.retry_interval_s) || 30, resend_interval: Number(b.resend_interval) || 0, - cert_alert_days: b.cert_alert_days != null ? Number(b.cert_alert_days) : 14, + cert_alert_days: b.cert_alert_days != null ? Number(b.cert_alert_days) : 0, channel_ids: Array.isArray(b.channel_ids) ? b.channel_ids : (b.channel_ids ? [b.channel_ids] : []), regions, request_headers: Object.keys(requestHeaders).length ? requestHeaders : null, @@ -567,7 +567,7 @@ export const dashboard = new Elysia() max_retries: Number(b.max_retries) || 0, retry_interval_s: Number(b.retry_interval_s) || 30, resend_interval: Number(b.resend_interval) || 0, - cert_alert_days: b.cert_alert_days != null ? Number(b.cert_alert_days) : 14, + cert_alert_days: b.cert_alert_days != null ? Number(b.cert_alert_days) : 0, channel_ids: Array.isArray(b.channel_ids) ? b.channel_ids : (b.channel_ids ? [b.channel_ids] : []), regions, request_headers: Object.keys(requestHeaders).length ? requestHeaders : null, diff --git a/apps/web/src/views/docs.ejs b/apps/web/src/views/docs.ejs index a870187..240eaf7 100644 --- a/apps/web/src/views/docs.ejs +++ b/apps/web/src/views/docs.ejs @@ -162,7 +162,7 @@ "max_retries": 2, // optional — retry N times before declaring DOWN. Default: 0 "retry_interval_s": 30, // optional — seconds between retries. Default: 30 "resend_interval": 10, // optional — re-alert every Nth consecutive DOWN beat. 0 = never. Default: 0 - "cert_alert_days": 14, // optional — alert when TLS cert is within N days of expiry. 0 disables. Default: 14 + "cert_alert_days": 0, // optional — alert when TLS cert is within N days of expiry. 0 disables. Default: 0 (disabled) "channel_ids": ["<uuid>"], // optional — notification channels to attach "query": { ... } // optional — see Query Language below } @@ -181,7 +181,7 @@ max_retriesnumberRetry a failing check this many times before posting a DOWN result. Default: 0. Max: 10. See Reliability. retry_interval_snumberSeconds between retries. Default: 30. resend_intervalnumberIf a monitor stays DOWN, re-fire a notification every Nth consecutive down beat. 0 disables resend. Default: 0. - cert_alert_daysnumberFire a separate cert notification when the TLS certificate is within N days of expiring. 0 disables. Default: 14. + cert_alert_daysnumberFire a separate cert notification when the TLS certificate is within N days of expiring. 0 disables. Default: 0 (disabled). channel_idsstring[]Notification channel IDs to attach. See Notifications. queryobjectQuery conditions — see below diff --git a/apps/web/src/views/partials/monitor-form.ejs b/apps/web/src/views/partials/monitor-form.ejs index 7a065bd..0a5d206 100644 --- a/apps/web/src/views/partials/monitor-form.ejs +++ b/apps/web/src/views/partials/monitor-form.ejs @@ -94,12 +94,19 @@ <% }) %> + <% + const allRetryGaps = [['5','5 seconds'],['15','15 seconds'],['30','30 seconds'],['60','1 minute'],['120','2 minutes'],['300','5 minutes']]; + const retryGaps = allRetryGaps.filter(([val]) => Number(val) >= minInterval); + const defaultRetryGap = String(Math.max(30, minInterval)); + const curRetryRaw = Number(monitor.retry_interval_s); + const curRetry = (curRetryRaw >= minInterval) ? String(curRetryRaw) : defaultRetryGap; + %>
@@ -116,9 +123,7 @@ <% const certDaysRaw = Number(monitor.cert_alert_days); - const certDaysSel = certDaysRaw === 0 ? '0' - : (certDaysRaw >= 1 && certDaysRaw <= 9) ? String(certDaysRaw) - : '7'; + const certDaysSel = (certDaysRaw >= 1 && certDaysRaw <= 9) ? String(certDaysRaw) : '0'; %>