From bd538b02c284b73c24f8315f3de9320bc8a78355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 21 May 2026 22:00:46 +0200 Subject: [PATCH] feat(integrations): add parked state + per-kind failure counters schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two additive columns on integration_statuses driving B4 + B7: - consecutive_failures_by_kind (jsonb) — bucketed counter { transient, reauth_required, persistent } so a transient hiccup followed by a persistent failure no longer resets the persistent streak counter. The single-column consecutive_failures stays for one release as a fallback; the alert ladder reads Math.max across buckets for backward compatibility. - persistent_failure_started_at (timestamp) — start of the current persistent failure streak. Once >24h elapse with the persistent bucket still non-zero, the status writer flips state to 'parked'. The state column stays free-form; 'parked' is recognised at the application layer. Existing rows have NULL in both new columns and are back-filled on the next write. --- .../0075_v1443_integration_park/migration.sql | 46 +++++++++++++++++++ prisma/schema.prisma | 24 +++++----- 2 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 prisma/migrations/0075_v1443_integration_park/migration.sql diff --git a/prisma/migrations/0075_v1443_integration_park/migration.sql b/prisma/migrations/0075_v1443_integration_park/migration.sql new file mode 100644 index 000000000..10636876d --- /dev/null +++ b/prisma/migrations/0075_v1443_integration_park/migration.sql @@ -0,0 +1,46 @@ +-- v1.4.43 W14 — IntegrationStatus parking + per-kind failure counters. +-- +-- Two additive columns on `integration_statuses`: +-- +-- 1. `consecutive_failures_by_kind` (jsonb) +-- A bucketed counter that increments only the bucket matching the +-- failure kind. The single-column `consecutive_failures` integer +-- stays in place for one release as a fallback (callers reading +-- the alert-ladder still use `Math.max(...buckets)`); it will be +-- removed in v1.4.44 once every reader is migrated. +-- +-- Default shape: { "transient": 0, "reauth_required": 0, +-- "persistent": 0 }. A `null` value means the +-- row predates this migration — the status writer back-fills it +-- on the next write by bucketing the old `consecutive_failures` +-- value into the current `state`. +-- +-- 2. `persistent_failure_started_at` (timestamp) +-- Wall-clock timestamp of the first persistent failure in the +-- current streak. Cleared whenever the persistent bucket resets +-- to zero. Once the gap between `now()` and this timestamp +-- exceeds 24h with the persistent bucket still non-zero, the +-- status writer flips `state` to `"parked"` — the integration +-- stops retrying and the UI surfaces a "Paused — reconnect +-- manually" pill that hits `/api/integrations/withings/resume` +-- to clear the park. +-- +-- Idempotent guards (`IF NOT EXISTS`) match the 0067 / 0070 / 0071 +-- pattern so reruns are safe. +-- +-- The `state` column is a free-form string today (deliberate per the +-- 0029 migration comment — "adding new sentinels in v1.5 doesn't +-- require a migration"). Adding `"parked"` as a recognised value is +-- therefore a no-op at the schema level; the application layer owns +-- the enum widening. +-- +-- Reversibility: down migration is `ALTER TABLE ... DROP COLUMN IF +-- EXISTS` on both new columns + a single UPDATE that clears any +-- `state = 'parked'` rows back to `'error_transient'`. Both columns +-- are nullable so existing rows keep working. + +ALTER TABLE "integration_statuses" + ADD COLUMN IF NOT EXISTS "consecutive_failures_by_kind" JSONB; + +ALTER TABLE "integration_statuses" + ADD COLUMN IF NOT EXISTS "persistent_failure_started_at" TIMESTAMP(3); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9e31aab28..2b130ce5c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1342,17 +1342,19 @@ model MedicationComplianceRollup { // `integration` values are bare strings ("withings", "moodlog") rather // than an enum so future integrations don't require a migration. model IntegrationStatus { - id String @id @default(cuid()) - userId String @map("user_id") - integration String // "withings" | "moodlog" - state String @default("connected") // "connected" | "error_reauth" | "error_transient" | "disconnected" - 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") - alertedAt DateTime? @map("alerted_at") // last time admin Telegram alert fired (idempotency guard) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + id String @id @default(cuid()) + userId String @map("user_id") + integration String // "withings" | "moodlog" + state String @default("connected") // "connected" | "error_reauth" | "error_transient" | "disconnected" | "parked" + 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 + 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) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade)