mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
v1.4.15 Phase B3, criteria 1 + 2 (atomic — they share migration + retry- policy types). Until this release the dispatcher fired channels best- effort and kept retrying every 60s on a 410 Gone — that meant a stale Web-Push subscription never recovered, a Telegram bot whose chat was deleted got the same treatment, and the user had no visibility into WHY their phone had stopped buzzing. Reliability rules now: - HARD reject (HTTP 410 web-push / ntfy, Telegram "chat not found" / "blocked by the user") → channel auto-disabled, disabled_reason captured, audit log notification.channel.auto_disabled written. Sender NEVER retried until the user clicks "Re-enable" in Settings. - SOFT reject (5xx, 429, network timeout) → exponential backoff: [30s, 5min, 30min, 2h]. Counter consecutive_failures increments per failure; after the 5th in-a-row the channel is auto-disabled with reason give_up_after_5_failures. - COOLDOWN window (next_retry_at > now) → dispatcher skips the channel without calling the sender — defeats the API-quota-burn loop a flapping upstream would otherwise create. Implementation: - Migration 0028 adds 6 columns to notification_channels (all nullable / zero-default → no-op for existing rows). - src/lib/notifications/retry-policy.ts: classifiers + frozen BACKOFF_SCHEDULE_MS + nextRetryAt() helpers. - src/lib/notifications/channel-state.ts: only writer of the new columns (recordChannelSuccess, recordChannelHardReject, recordChannelTransientFailure, reEnableChannel, isChannelInCooldown). Each transient/hard path writes audit. - Senders return a structured SendOutcome instead of bare booleans. - Dispatcher: cooldown-skip → success → hard-reject → transient/ give-up. Sender-throws absorbed as transient failures. Tests: +20 unit tests (retry-policy.test.ts, channel-state.test.ts, dispatcher.test.ts). 883/883 unit tests pass; typecheck clean; lint clean (no new warnings). Pre-existing bug fix: src/app/api/settings/telegram/test/route.ts had if (!ok) against a SendMessageResult object — always truthy, so the route never reported test-send failures. Now reads result.ok. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
35 lines
1.8 KiB
SQL
35 lines
1.8 KiB
SQL
-- v1.4.15 Phase B3: notification reliability metadata.
|
|
--
|
|
-- Until this release the dispatcher fired channels best-effort and the
|
|
-- only feedback signal we kept was a Wide-Event warning per failure.
|
|
-- That meant a stale Web-Push subscription kept burning bandwidth on
|
|
-- every dispatch (a 410 from FCM is a permanent reject — re-trying it
|
|
-- never recovers), a Telegram bot whose chat was deleted got the same
|
|
-- treatment, and the user never saw the channel's actual health.
|
|
--
|
|
-- These columns let the dispatcher:
|
|
-- 1. Auto-disable a channel on a hard reject (HTTP 410, Telegram
|
|
-- "chat not found" or "blocked by the user", ntfy 410), with the
|
|
-- reason captured for the Settings UI.
|
|
-- 2. Apply exponential backoff [30s, 5min, 30min, 2h] to soft errors
|
|
-- (5xx, 429, network timeout) instead of hammering a flapping
|
|
-- upstream — and after 5 consecutive transient failures, treat
|
|
-- the channel as effectively dead and auto-disable it too.
|
|
-- 3. Surface "Active / Auto-disabled / Sending paused" + last
|
|
-- success / failure timestamps in Settings → Notifications, with
|
|
-- a "Re-enable" button that clears `disabled_reason` +
|
|
-- `consecutive_failures` and a "Send test" button.
|
|
--
|
|
-- All columns are nullable / zero-default so the migration is a no-op
|
|
-- for existing rows: a healthy channel reports
|
|
-- consecutive_failures=0, last_success_at=null, disabled_reason=null,
|
|
-- which renders as "Active (no recent send recorded)" in the UI.
|
|
|
|
ALTER TABLE "notification_channels"
|
|
ADD COLUMN "disabled_reason" TEXT,
|
|
ADD COLUMN "consecutive_failures" INTEGER NOT NULL DEFAULT 0,
|
|
ADD COLUMN "last_success_at" TIMESTAMP(3),
|
|
ADD COLUMN "last_failure_at" TIMESTAMP(3),
|
|
ADD COLUMN "last_failure_reason" TEXT,
|
|
ADD COLUMN "next_retry_at" TIMESTAMP(3);
|