mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
v1.4.15 Phase B2, criterion 1. Until this release the Settings →
Integrations card showed "Connected / Last sync 23 min ago" and
nothing else. A user whose Withings refresh token had revoked saw a
green badge; a user whose moodLog instance was down saw the same
green badge with a slightly older timestamp. The signal was useless.
This commit adds:
- New `IntegrationStatus` table (`integration_statuses` migration
`0029`). One row per (user, integration) capturing `state`
(`connected | error_transient | error_reauth | disconnected`),
`lastSuccessAt`, `lastAttemptAt`, encrypted `lastError`,
`consecutiveFailures` counter, `alertedAt` window-guard.
- `src/lib/integrations/status.ts` — the only writers
(`recordSyncSuccess`, `recordSyncFailure`, `markReauthRequired`,
`markReconnected`, `markDisconnected`) plus a read-only
`getIntegrationStatus()` for the API. The error column is
AES-256-GCM encrypted at rest because Withings/moodLog 401s can
echo URL/apiKey fragments — we can't ship that into a future
backup tarball.
- `GET /api/integrations/status` — single roundtrip returns both
Withings and moodLog snapshots plus the global threshold (so the
UI's "{n}/{threshold}" string is server-sourced and rotates with
the env var).
- Settings → Integrations cards both gain an
`IntegrationStatusBanner` row above the credentials form: state
badge ("Connected" / "Reconnect required" / "Error" /
"Disconnected"), `{n}/{threshold} consecutive failures` chip,
last-success / last-attempt times, and a destructive-toned
last-error line. The banner is suppressed entirely when the
integration is fresh-connected with no history (no false alarms).
Tests: +20 unit (status helper threshold + alert-window state machine,
banner rendering across all four states) + 8 integration
(`tests/integration/integration-status.test.ts`) confirming
encrypted-at-rest, audit-log row shape, alert idempotency window,
and CASCADE-delete on User. 116 files / 883 unit pass, 11 integration
files / 39 pass.
Encryption uses the existing `src/lib/crypto.ts` AES-256-GCM helper —
no new key material. The `consecutive_failures` counter and
`alerted_at` window are wired to the dispatcher in the next commit
(`feat(integrations): admin Telegram alert on persistent sync
failure`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
53 lines
2.5 KiB
SQL
53 lines
2.5 KiB
SQL
-- v1.4.15 Phase B2: per-(user, integration) sync status row.
|
|
--
|
|
-- The Settings → Integrations card needs more than a "connected yes/no"
|
|
-- bit to be useful: it must show last-success time, last-attempt time,
|
|
-- the most-recent error, and a "needs reconnect" state when the
|
|
-- refresh-token grant has revoked. A separate table is preferable to
|
|
-- piling more columns onto `users` / `withings_connections` because:
|
|
--
|
|
-- 1. moodLog credentials live on `users`, Withings tokens live on
|
|
-- `withings_connections` — neither is a good home for the OTHER
|
|
-- integration's status.
|
|
-- 2. Future integrations (HealthKit, Garmin, etc.) get a row here
|
|
-- without a schema migration per integration.
|
|
-- 3. The (userId, integration) UNIQUE constraint upserts cleanly
|
|
-- from the `recordSyncSuccess` / `recordSyncFailure` helpers in
|
|
-- `src/lib/integrations/status.ts`.
|
|
--
|
|
-- Encryption: `last_error` is AES-256-GCM encrypted at the application
|
|
-- layer (see `src/lib/crypto.ts`) because some upstream errors echo
|
|
-- back fragments of the user's URL or apiKey (moodLog 401 responses
|
|
-- can include the supplied Authorization header in the body). Keeping
|
|
-- this column ciphertext-only matches the Withings/moodLog credential
|
|
-- treatment elsewhere in the schema.
|
|
--
|
|
-- `consecutive_failures` resets to 0 on the next success and feeds the
|
|
-- admin-Telegram alert threshold (default 3, see PERSISTENT_FAILURE_*
|
|
-- constants in `src/lib/integrations/status.ts`). `alerted_at` is the
|
|
-- idempotency guard so the alert fires once per failure burst, not
|
|
-- once per failure.
|
|
|
|
CREATE TABLE "integration_statuses" (
|
|
"id" TEXT NOT NULL,
|
|
"user_id" TEXT NOT NULL,
|
|
"integration" TEXT NOT NULL,
|
|
"state" TEXT NOT NULL DEFAULT 'connected',
|
|
"last_success_at" TIMESTAMP(3),
|
|
"last_attempt_at" TIMESTAMP(3),
|
|
"last_error" TEXT,
|
|
"consecutive_failures" INTEGER NOT NULL DEFAULT 0,
|
|
"alerted_at" TIMESTAMP(3),
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
|
|
CONSTRAINT "integration_statuses_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
CREATE INDEX "integration_statuses_user_id_idx" ON "integration_statuses"("user_id");
|
|
CREATE INDEX "integration_statuses_state_idx" ON "integration_statuses"("state");
|
|
CREATE UNIQUE INDEX "integration_statuses_user_id_integration_key" ON "integration_statuses"("user_id", "integration");
|
|
|
|
ALTER TABLE "integration_statuses" ADD CONSTRAINT "integration_statuses_user_id_fkey"
|
|
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|