From b3d0d3a87b298cf14f420f850ab2ecc55e602635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 9 May 2026 21:10:20 +0200 Subject: [PATCH] docs(planning): record phase B2 completion in STATE.md + report Status block + detailed report for v1.4.15 Phase B2 (Withings + moodLog sync robustness). All four criteria green on origin/main: - 4db72a8 status UI + IntegrationStatus table - b290a77 refresh-token reauth wiring - 604dff0 admin Telegram alert >=3 consecutive - 2fbf56d audit-log entry on every failure Co-Authored-By: Claude Opus 4.7 (1M context) --- .planning/STATE.md | 51 ++++++++++++++-- .planning/phase-B2-report.md | 110 +++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 .planning/phase-B2-report.md diff --git a/.planning/STATE.md b/.planning/STATE.md index d635c0066..c05ac74ba 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -264,12 +264,55 @@ pattern A2 + A4 documented. v1.4.16 should adopt ### B2 — Withings + moodLog sync robustness -- [ ] Status UI in Settings → Integrations -- [ ] Refresh token rotation handling -- [ ] Telegram-notify on persistent failure (if admin enabled the channel) -- [ ] Audit-log entry on failure +- [x] Status UI in Settings → Integrations (`4db72a8`) +- [x] Refresh token rotation handling (`b290a77`) +- [x] Telegram-notify on persistent failure (if admin enabled the channel) (`604dff0`) +- [x] Audit-log entry on failure (`2fbf56d`) - Detailed report: `.planning/phase-B2-report.md` +#### B2 status block — 2026-05-09T21:09+02:00 + +4 / 4 criteria shipped to `origin/main`. New `IntegrationStatus` +table (migration `0029_integration_status`) per (user, integration) +with `state` (`connected | error_transient | error_reauth | +disconnected`), `lastSuccessAt`, `lastAttemptAt`, encrypted +`lastError` (AES-256-GCM), `consecutiveFailures` counter, and 24h +`alertedAt` window-guard. Single writer at +`src/lib/integrations/status.ts`; Withings + moodLog sync flows wire +through it for both success and failure. `GET /api/integrations/status` +returns both snapshots in one round-trip; the Settings → +Integrations cards both render an `IntegrationStatusBanner` above +their credentials form (state badge, `{n}/{threshold}` chip, +last-success / last-attempt times, last-error line). + +Withings refresh-token failures classified by upstream status code +(100/101/102 + 200..299 → `error_reauth`, else transient). moodLog +401/403 → `error_reauth`. Parked connections short-circuit inside +`getValidToken()` and `syncMoodLogEntries()` so scheduled syncs +no longer hammer the upstream. OAuth callback / disconnect / +re-save-credentials routes flip state to match. + +Persistent failures (≥3 consecutive, configurable via +`INTEGRATION_FAILURE_ALERT_THRESHOLD`) trigger one admin Telegram +alert per burst via `dispatchNotification(SYSTEM_ALERT)` — no new +sender / channel type (B3 owns dispatcher reliability). 24h +`alertedAt` re-alert window prevents flapping. + +Tests: +35 unit (16 helper state machine, 7 alert formatter, 4 banner +SSR) + 8 integration (real Postgres testcontainer covering +encrypted-at-rest, audit-log shape, alert idempotency, reauth/ +reconnect flow, CASCADE delete). 890 / 890 unit pass; 31 / 31 +integration pass. + +Cross-agent: my initial `prisma migrate dev` ran into B3's earlier +schema drift; rewrote the migration file by hand to contain only the +`integration_statuses` DDL and renumbered to `0029_`. B3's +`87a40fd` commit absorbed the schema's `IntegrationStatus` model +addition as a side effect of overlapping `git add -A` runs — my +commits therefore carry the migration + helper + API + UI + tests + +sync wiring, not the schema diff. Recommendation for v1.4.16 +(echoing A2 / A4): one git worktree per parallel agent. + ### B3 — Notification reliability - [x] Auto-disable channel on persistent 410 / hard rejects diff --git a/.planning/phase-B2-report.md b/.planning/phase-B2-report.md new file mode 100644 index 000000000..38037c732 --- /dev/null +++ b/.planning/phase-B2-report.md @@ -0,0 +1,110 @@ +# Phase B2 — Withings + moodLog sync robustness + +**Status:** done. 4 atomic commits on `origin/main`, all green. + +## Commits + +- `4db72a8` — `feat(integrations): connection-state + sync-error UI in Settings → Integrations` +- `b290a77` — `fix(integrations): refresh-token failure marks integration as needing re-auth` +- `604dff0` — `feat(integrations): admin Telegram alert on persistent sync failure (>=3 consecutive)` +- `2fbf56d` — `feat(audit): sync failures logged with structured meta` + +## What landed + +A new `IntegrationStatus` table (migration `0029`) gives every (user, +integration) pair a row with `state` (`connected | error_transient | +error_reauth | disconnected`), `lastSuccessAt`, `lastAttemptAt`, +encrypted `lastError`, `consecutiveFailures` counter, and `alertedAt` +window-guard. The single writer is `src/lib/integrations/status.ts`, +exposing `recordSyncSuccess` / `recordSyncFailure` / +`markReauthRequired` / `markReconnected` / `markDisconnected` / +`getIntegrationStatus` / `isReauthRequired`. + +`GET /api/integrations/status` returns both Withings + moodLog +snapshots in one round-trip, including the global threshold so the UI +"{n}/{threshold} consecutive failures" string is server-sourced. + +The Settings → Integrations cards both render an +`IntegrationStatusBanner` above the credentials form (state badge, +counter chip, last-success / last-attempt times, destructive-toned +last-error line). The banner self-suppresses when the integration is +fresh-connected with no history. + +The Withings + moodLog sync flows (`src/lib/withings/sync.ts`, +`src/lib/moodlog/sync.ts`) now call the helpers on success / failure. +Refresh-token failures classified by Withings status code +(100/101/102/200..299 → `error_reauth`, everything else → transient); +moodLog 401/403 → `error_reauth`. Parked connections short-circuit +inside `getValidToken()` and `syncMoodLogEntries()` so scheduled +pg-boss runs no longer burn quota on a bad credential. The +`/api/withings/callback`, `/api/withings/disconnect`, +`PUT /api/settings/moodlog`, and `DELETE /api/settings/moodlog` routes +all flip the state machine to match (`markReconnected` / +`markDisconnected`). + +Persistent failures (≥3 consecutive, configurable via +`INTEGRATION_FAILURE_ALERT_THRESHOLD`) trigger a single admin Telegram +alert per failure burst via the existing dispatcher +(`SYSTEM_ALERT` event type — no new sender, no new channel type, B3 +owns dispatcher reliability). A 24h `alertedAt` window prevents flapping +integrations from paging on every retry. + +Every failure writes one `AuditLog` row with structured meta +(`integration`, `kind`, `errorCode`, `message`, `attemptNumber`, +`state`). Successes are intentionally not audited — `lastSuccessAt` +on the IntegrationStatus row tracks them at one row per integration +per user instead. + +The `lastError` column is AES-256-GCM encrypted at rest because +Withings/moodLog 401 responses can echo URL/apiKey fragments — we +don't want those to land in a future backup tarball. + +## Tests + +- `src/lib/integrations/__tests__/status.test.ts` — 16 unit tests for + threshold + alert-window state machine. +- `src/lib/integrations/__tests__/admin-alert.test.ts` — 7 unit tests + for the Telegram alert payload formatter (extracted as a pure + function so it's testable without Prisma/dispatcher). +- `src/components/settings/__tests__/integrations-section.test.tsx` — + 4 SSR snapshot tests, one per state, locking the banner contract. +- `tests/integration/integration-status.test.ts` — 8 real-Postgres + cases: encrypted-at-rest, audit-log shape, alert idempotency, + reauth parking, reconnect flow, CASCADE delete on User. + +Totals: **890 / 890 unit pass** (was 817 baseline; +35 mine + 38 from +B3 racing in alongside). **31 / 31 integration pass** (8 mine). + +Typecheck: 0 errors in B2 files. Lint: 0 errors. + +## Constraints respected + +- No new dependencies added. +- No new dispatcher senders (B3's territory) — only the existing + `dispatchNotification()` is called. +- No edits to admin-backups files (B1's territory) or workflows (C3). +- Migration `0029_integration_status` committed alongside the schema. +- All sensitive token data stays AES-256-GCM encrypted via + `src/lib/crypto.ts` (the new `lastError` column included). +- Co-Author trailer on every commit. No `--no-verify`. Pre-commit + hooks passed. + +## Cross-agent observations + +- Sibling agents (B3, B-mobile, B1) racing on the same working tree + caused two side-effects: + 1. My initial `prisma migrate dev` saw schema-vs-DB drift from + B3's earlier work and emitted irrelevant ALTER statements in the + migration file. Hand-rewrote the migration to contain only the + `integration_statuses` DDL; renamed the directory from Prisma's + timestamp-default to the project's sequential-numeric convention + (`0029_`) and updated `_prisma_migrations.migration_name` to + match. + 2. B3's `87a40fd` commit picked up the IntegrationStatus model from + `schema.prisma` because my stash overlapped B3's index when they + ran `git add -A`. The schema diff is therefore already on main + under B3's commit — my commits only carry the migration, + helper, API, UI, tests, and sync wiring. +- Recommendation for v1.4.16 (echoing A2/A4): each parallel agent + should run in its own `superpowers:using-git-worktrees` worktree + to avoid this class of cross-commit bleed.