mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
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: -4db72a8status UI + IntegrationStatus table -b290a77refresh-token reauth wiring -604dff0admin Telegram alert >=3 consecutive -2fbf56daudit-log entry on every failure Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
2fbf56dc16
commit
b3d0d3a87b
2 files changed
+157
-4
No files matched your search
+47
-4
@@ -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
|
||||
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user