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) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-09 21:10:20 +02:00
1 parent 2fbf56dc16
commit b3d0d3a87b
2 files changed
+157 -4

No files matched your search

+47 -4
View File
@@ -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
+110
View File
@@ -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.