Commit Graph
2 Commits
Author SHA1 Message Date
Marc-André Bombeck 5a7484658e feat(withings): surface parked state + manual resume CTA
Pill UI gains a fifth state — 'parked' — for the v1.4.43 W14 24h
persistent-failure park. Copy reads 'Pausiert — manuell wieder
verbinden' (DE) / 'Paused — reconnect manually' (EN); chip uses the
dracula-orange tokens (heavier than warning yellow, lighter than the
red reconnect pill).

The Withings card renders a 'Wieder verbinden' / 'Reconnect' banner
under the pill when the state is parked. Clicking POSTs to the new
/api/integrations/withings/resume endpoint (rate-limited 5/min per
user, same envelope as the test-connection endpoint). The endpoint
calls resumeIntegrationFromPark which flips state back to connected
and zeroes every per-kind bucket; the per-card status query
invalidates so the pill flips back without a page refresh.

Locale parity across all six locales (en/de/es/fr/it/pl); end-to-end
integration test against real Postgres pins both the resume flow and
the per-kind bucket increment contract.
2026-05-21 22:19:29 +02:00
Marc-André BombeckandClaude Opus 4.7 2fbf56dc16 feat(audit): sync failures logged with structured meta
v1.4.15 Phase B2, criterion 4. Every IntegrationStatus failure path
now writes a single AuditLog row with a documented schema, and this
commit lights up the real-Postgres regression guard for it:

  action  : "integrations.sync.failed"
  userId  : <subject>
  details : {
    integration:    "withings" | "moodlog",
    kind:           "transient" | "reauth_required",
    errorCode:      string | null,
    message:        string,           // plaintext (not encrypted —
                                       // audit table is admin-only)
    attemptNumber:  number,           // 1..N consecutive
    state:          "error_transient" | "error_reauth"
  }

Successes are NOT audited — that path would be far too noisy
(every webhook-triggered sync is a row, ~ten times a day per active
user) and the integration_statuses.lastSuccessAt column already
tracks the same signal at one row per integration per user.

The integration test in tests/integration/integration-status.test.ts
provides 8 cases against a real Postgres testcontainer:

  - failure writes both the integration_statuses row AND a matching
    AuditLog entry, with the structured meta validated field-by-field
  - lastError is encrypted at rest (column never matches plaintext)
  - getIntegrationStatus() decrypts cleanly on read
  - success clears the streak AND does NOT write an audit row
  - admin Telegram alert fires once per burst (24h re-alert window)
  - reauth_required parks the integration so isReauthRequired() is true
  - markReconnected() clears the parked state without writing a fresh
    success — that's the next sync's job
  - markDisconnected() preserves the row as a tombstone but wipes
    error state
  - CASCADE delete on User removes the IntegrationStatus row

Setup file additions: integration_statuses added to the truncate-list
in tests/integration/setup.ts (already shipped under commit 4db72a8;
this commit only adds the test file that consumes it).

Tests: 11 / 39 integration pass. The dispatcher is module-mocked so
the test doesn't actually hit Telegram — we only assert that the
helper INVOKED the dispatcher with the right payload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 21:07:44 +02:00