diff --git a/prisma/migrations/0077_v1447_drop_legacy_consecutive_failures/migration.sql b/prisma/migrations/0077_v1447_drop_legacy_consecutive_failures/migration.sql new file mode 100644 index 000000000..d7f134683 --- /dev/null +++ b/prisma/migrations/0077_v1447_drop_legacy_consecutive_failures/migration.sql @@ -0,0 +1,49 @@ +-- v1.4.47 W1 — drop the legacy `consecutive_failures` column on +-- `integration_statuses`. +-- +-- v1.4.43 W14 introduced `consecutive_failures_by_kind` (JSONB) as +-- the bucketed per-FailureKind counter and kept the single-column +-- `consecutive_failures` integer for one release as a fallback so +-- v1.4.43 readers wouldn't break mid-deploy. v1.4.45 W10 closed the +-- write-time gap (back-fill increment posts the bucket and the legacy +-- column at the same N+1 value). v1.4.47 W1 drops the legacy column +-- now that every active integration in production has written at +-- least one new failure or success since v1.4.43 shipped (>24h ago), +-- so every row's `consecutive_failures_by_kind` is populated and the +-- legacy integer is redundant. +-- +-- After this migration the alert-ladder reader uses +-- `Math.max(...Object.values(buckets))` exclusively — the legacy +-- column is no longer consulted. `recordSyncFailure` and friends +-- stop writing to `consecutive_failures` entirely (the corresponding +-- application-level cleanup in `src/lib/integrations/status.ts` +-- lands in the same release). +-- +-- Idempotent guard (`IF EXISTS`) matches the 0075 pattern so reruns +-- against an environment that has already dropped the column are +-- safe. +-- +-- Reversibility (down-migration): +-- +-- ALTER TABLE "integration_statuses" +-- ADD COLUMN IF NOT EXISTS "consecutive_failures" INTEGER +-- NOT NULL DEFAULT 0; +-- +-- UPDATE "integration_statuses" +-- SET "consecutive_failures" = COALESCE( +-- GREATEST( +-- ("consecutive_failures_by_kind"->>'transient')::int, +-- ("consecutive_failures_by_kind"->>'reauth_required')::int, +-- ("consecutive_failures_by_kind"->>'persistent')::int +-- ), +-- 0 +-- ) +-- WHERE "consecutive_failures_by_kind" IS NOT NULL; +-- +-- The down-script restores the legacy counter to `Math.max(...buckets)` +-- — the same value the alert ladder reads from the buckets — so a +-- rollback lands every row at the post-v1.4.43 counter value without +-- losing the streak's true age. + +ALTER TABLE "integration_statuses" + DROP COLUMN IF EXISTS "consecutive_failures"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5c94f1d9b..e1a03e68e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1374,11 +1374,12 @@ model MedicationComplianceRollup { // - consecutive failure counter (resets on success) // // `recordSyncSuccess` / `recordSyncFailure` helpers in -// `src/lib/integrations/status.ts` are the only writers. The -// `consecutiveFailures` counter feeds the admin-Telegram alerting -// threshold (default 3). State `error_reauth` parks scheduled syncs -// until the user reconnects (the sync entry-points consult this row -// before doing any external work). +// `src/lib/integrations/status.ts` are the only writers. The per-kind +// `consecutiveFailuresByKind` JSON bucket feeds the admin-Telegram +// alerting threshold (default 3) via `Math.max(...Object.values(...))`. +// State `error_reauth` parks scheduled syncs until the user reconnects +// (the sync entry-points consult this row before doing any external +// work). // // `integration` values are bare strings ("withings", "moodlog") rather // than an enum so future integrations don't require a migration. @@ -1390,7 +1391,9 @@ model IntegrationStatus { lastSuccessAt DateTime? @map("last_success_at") lastAttemptAt DateTime? @map("last_attempt_at") lastError String? @map("last_error") // AES-256-GCM encrypted (may contain token-fragment metadata) - consecutiveFailures Int @default(0) @map("consecutive_failures") // v1.4.44 will drop; per-kind counter below is the source of truth + // v1.4.47 W1 — legacy single-column `consecutiveFailures` integer was + // dropped (migration 0076). The per-kind bucket below is now the + // sole source of truth for the alert ladder. consecutiveFailuresByKind Json? @map("consecutive_failures_by_kind") // v1.4.43 W14 — { transient, reauth_required, persistent } persistentFailureStartedAt DateTime? @map("persistent_failure_started_at") // v1.4.43 W14 — start of the current persistent streak (drives >24h park) alertedAt DateTime? @map("alerted_at") // last time admin Telegram alert fired (idempotency guard)