Commit Graph
1052 Commits
Author SHA1 Message Date
Marc-André Bombeck 5cdedeae5a docs(planning): record Withings 403 scope-skip fix in v1.4.26 backlog
Logs the production-alarm hotfix shipped in `fec02aa` under a new
"Resolved by v1.4.26" section so the backlog stays honest about what
landed on develop after the v1.4.25 tag.
2026-05-15 07:32:56 +02:00
Marc-André Bombeck fec02aa573 fix(withings): skip activity + sleep sync when OAuth scope lacks user.activity
Legacy v1.4.24-and-earlier Withings connections sit on `user.metrics`
only. W17b/c added `syncUserActivity` + `syncUserSleep` against the
scope-gated `/v2/measure?action=getactivity` and `/v2/sleep?action=get`
endpoints without a scope guard, so every cron tick for a legacy user
hit a guaranteed 403. The catch-block classified the 403 as transient
(the refresh token still worked; only the resource call failed), pg-boss
retried, and the 3-strike admin alert paged Telegram for every affected
user.

Both routines now short-circuit at entry: read `WithingsConnection.scope`,
call `hasActivityScope` from `src/lib/withings/client.ts`, and on miss
park at `error_reauth` via `recordSyncFailure({ kind: "reauth_required",
errorCode: "scope_missing" })` and return 0. The Settings reconnect
banner already in place tells the user what to do; subsequent cron ticks
short-circuit cheaply on `isReauthRequired`.

Defence-in-depth: a 403 reaching the catch-block (race where scope was
revoked between OAuth and the next sync without invalidating the refresh
token) now classifies as `reauth_required` instead of `transient`, so
pg-boss stops retrying and the 3-strike alert stays quiet.

Tests: +4 scope-skip / 403-classification cases per routine (8 total),
defaulting the existing field-mapping + idempotency tests to a
scope-is-fine connection so they stay focused on the write path.
2026-05-15 07:32:35 +02:00
Marc-André Bombeck ac2142e19a docs(planning): APNs .p8 key handling policy 2026-05-15 07:27:20 +02:00
Marc-André Bombeck e8fb5e04fe docs(apple): App Store Connect submission checklist 2026-05-15 07:27:17 +02:00
Marc-André Bombeck caf0b9561a docs(planning): Codex audit prompt for v1.4.25 2026-05-14 23:57:30 +02:00
Marc-André Bombeck d708c94e55 docs(audit): v1.4.25 summary with 0-10 scores 2026-05-14 23:57:27 +02:00
Marc-André Bombeck 15ac4c5739 docs(ios-handoff): E-Closeout — finalize README with real navigation
Replace the seed skeleton with the final navigation map for the
21-file doc-pack.

Inventory: 21 rows pinned against the actual filenames + final
token estimates (total ~89400 tokens / ~57000 words).

Question → File map: 38 real entries mapping concrete iOS-Claude
questions to file + section anchors. Covers source-priority,
MeasurementType enum, MDR boundary, APNs flow, Withings OAuth,
HealthKit batch ingest, Coach SSE, Health Score, PR detection,
migrations 0051-0060, OpenAPI hard-flip gate, error codes,
Idempotency-Key, rate-limiting, PROMPT_VERSION,
RESEARCH_MODE_DISCLAIMER_VERSION, TanStack Query, chart hex literals,
locale keys, dev server, Simulator API token, v1.4.26 inventory.

Reading-orders-by-goal: 16 scenarios with file lists + estimated
cumulative tokens. Cold-start, tag-a-release, new endpoint, Coach
feature, Apple Health sync, workout ingest, GLP-1 work (critical
research-first), pre-PR gate, screen design, Settings → Sources,
Health Score tile, Withings reconnect, onboarding wizard, CI setup.

Stop-here-markers index: per-file summary of what each file's STOP
HERE block (or inline warnings) covers, so iOS-Claude can lookup
and exit.

TL;DR refresh: 2 sentences pinning v1.4.25 stack + v1.5 = iOS +
deeper Apple Health.

Cross-link audit: every <file>.md reference in every sibling now
resolves to a real file. 16 broken references found + patched in
the prior commit.
2026-05-14 23:45:44 +02:00
Marc-André Bombeck 759028ad8f docs(ios-handoff): E-Closeout — cross-link patch across sibling files
Sibling docs (00..20 except README) referenced 16 file names that don't
exist in the final pack — names like 10-coach-pipeline.md, 12-ai-coach.md,
15-withings-bridge.md, 17-personal-records.md, 18-onboarding.md,
19-deploy-runtime.md, 02-architecture.md, 03-system-overview.md,
10-architecture.md, 12-ai-providers.md, 13-medical-content.md,
09-ui-state.md. Every one mapped to a real file in the final pack;
patched all cross-links in:

- 00-philosophy.md            (STOP HERE table)
- 01-repo-tour.md             (deploy link + STOP HERE table)
- 02-server-architecture.md   (6 Detail-in references + STOP HERE table)
- 03-api-contracts.md         (STOP HERE table)
- 04-data-model.md            (prerequisites + What is NOT in this file)
- 05-auth-flows.md            (What is NOT in this file)
- 06-ios-responsibilities.md  (prerequisites)
- 07-server-responsibilities.md (prerequisites)
- 08-locked-contracts.md      (What is NOT in this file)
- 11-web-ui-tour.md           (prerequisites)
- 12-design-system.md         (prerequisites)
- 13-state-management.md      (prerequisites)
- 14-coach-mental-model.md    (prerequisites)
- 17-error-handling.md        (What is NOT in this file)
- 18-pattern-cookbook.md      (prerequisites)
- 19-i18n-system.md           (prerequisites)
- 20-glossary.md              (multiple Coach + provider references)

Every cross-link in the doc-pack now resolves to an existing file or to
a section heading inside an existing file.
2026-05-14 23:44:17 +02:00
Marc-André Bombeck b4a98ef38e docs(ios-handoff): E-Closeout — recommended flow + research pointers
Two new files closing the v1.4.25 / v1.5 iOS handoff doc-pack.

09-recommended-flow.md (~6000 tokens)
- First-time setup recipe (clone, dev server, API token, Simulator pointing)
- Contract-diff loop (openapi:check + Swift codegen regen)
- Touch-disjoint discipline for parallel iOS sub-agents
- Marc-Voice commit-message style guide with examples + counterexamples
- Marathon-pattern handoff section (non-negotiables, pre-flight rule, research-first)
- Pre-release Quality Gate checklist (8 reviewers + reconcile pass, modelled on W21)
- Mock vs hit-live test strategy (unit / integration / e2e split)
- CI workflow surfaces (server-side vs iOS-side, the cross-side handoff)
- 10 iOS-only gotchas the server log never sees (Simulator HealthKit,
  Withings deep-link return, RESEARCH_MODE_DISCLAIMER_VERSION drift,
  SSE streaming via URLSession.bytes, idempotency-key lifecycle, etc.)

10-research-pointers.md (~3500 tokens)
- Ecosystem research index (Apple Health, GLP-1, wearables comparison, Withings)
- UX + design research index (health-score provenance, insights sub-pages,
  source-priority two-axis, zielwerte redesign)
- Feature-specific research (W8d enums, W14b onboarding, W14c native Coach
  prompts, W16b workouts, W16c PR detection, W17b-c Withings v2)
- W21 review findings index with what's still pending in v1.4.26
- W10 baseline (historical context)
- Pointer to .planning/phase-W21-reconcile-plan.md as the canonical post-W21
  inventory + the 7 Fix-* surface map
- Research-first directive (which feature classes require a research note
  before code lands) + the format convention for new notes
2026-05-14 23:42:44 +02:00
Marc-André Bombeck 24d88858e9 docs(ios-handoff): D-AI Layer — iOS and server responsibility boundaries
The remaining two files of D. 06-ios-responsibilities.md enumerates the five
domains the iOS app owns (HealthKit, Keychain, APNs, offline cache, deep
links) plus the MDR-critical 'don't cross this line' warnings.
07-server-responsibilities.md is the inverse — 22 domains the server
already handles, so iOS Claude knows what NOT to reinvent.
2026-05-14 23:35:22 +02:00
Marc-André Bombeck 39c0242a85 docs(ios-handoff): B-Data&Auth — data-model + auth + locked-contracts + errors + glossary 2026-05-14 23:33:31 +02:00
Marc-André Bombeck a756c51be8 docs(ios-handoff): A-Foundation — README skeleton + philosophy + repo-tour + architecture + API contracts 2026-05-14 23:33:20 +02:00
Marc-André Bombeck 9bfbbdf345 docs(ios-handoff): C-UI&State — web-UI tour + design system + state mgmt + pattern cookbook + i18n 2026-05-14 23:32:12 +02:00
Marc-André Bombeck 24cd5cece1 docs(ios-handoff): D-AI Layer — coach, insights, health-score mental model
Three of D's five files. The Coach + Insights + Health-Score surfaces are
server-resident; iOS Claude must consume, not reimplement. Documents the
layered prompt builder, GROUND RULES 1-15 (including W4d/W19c safety
contracts that hold the EU MDR line), the safety-contract matrix loader,
cache-invalidation rules, source-attribution provenance, and the
deterministic asOf discipline introduced in v1.4.25 Fix-G.
2026-05-14 23:30:50 +02:00
Marc-André Bombeck 6153be04cf docs(ios-handoff): C-UI&State design system + state mgmt + i18n 2026-05-14 23:28:12 +02:00
Marc-André Bombeck 0a9257357b docs(planning): W22 release-redo phase report 2026-05-14 23:20:18 +02:00
Marc-André Bombeck 49f71c9215 chore(release): expand v1.4.25 with Wave 4-5 and reconcile features
Rewrites the v1.4.25 CHANGELOG section to cover the ~90 commits since
v1.4.24 across onboarding rebuild, GLP-1 medication tracking end-to-end
(drug knowledge, Research Mode with MDR acknowledgment dialog,
estimated drug-level chart, side-effect taxonomy, cadence + compliance,
EMA titration ladder, pen-and-vial inventory with a 30-day clock),
Personal Records detection worker + badge + push opt-in, workout batch
ingest, Withings Activity + Sleep v2 syncs, EMA drug knowledge layer,
log-redaction path-segment rule that closes the Withings webhook
secret leak, GLP-1 convenience endpoint hardening, native first-party
Coach + insights system prompts across six locales with a refusal-probe
matrix, OpenAPI drift-gate hard-fail flip, 380 dead i18n keys removed,
shared helpers + section wrapper consolidations, dashboard top-tile
polish, design polish to WCAG 2.5.5 + AA contrast.

Updates the test count footer to the current 3828 unit / 344 files /
~170 integration / 9 new migrations (0051-0059) + 1 hardening (0060).
Adds the refusal-probe matrix metrics (1800+ assertions x 15 GROUND
RULES x 6 locales x 20+ paraphrasings).

Replaces the stale "Deferred to v1.4.26" section. Items that already
shipped in Wave 4-5 (OpenAPI hard-flip, onboarding rebuild, native
FR/ES/IT/PL Coach prompts, PR detection worker, workout ingest,
Withings Activity + Sleep v2) are out of the deferred list and into
Added / Changed / Fixed / Security / Refactor. The remaining v1.4.26
backlog reads accurately.
2026-05-14 23:19:26 +02:00
Marc-André Bombeck 068b49e709 docs(planning): Fix-N reconcile phase report
Closes the W21 reconcile pass. Captures the six commits, the 49 new
test assertions, three documented deviations (inventory-section
keeping its details shell, medication-form parseDose left alone,
apiError signature broadening), and final-gate evidence.
2026-05-14 23:12:05 +02:00
Marc-André Bombeck df719caa2f fix(medications): derive side-effect category server-side + drift-guard test
simp-M1 — the side-effect category and entry tuples in the Zod
validators no longer restate the Prisma enum keys verbatim. Both
arrays now derive from MedicationSideEffectCategory and
MedicationSideEffectEntry via z.nativeEnum, so adding a new entry
only requires touching the Prisma schema + the taxonomy mapping.

code-M1 — drift-guard test asserts the three sources (Prisma enums,
taxonomy module, validators) cover the same keysets. Catching drift
at test-time prevents the silent 422-vs-DB-accept mismatch that bit
the previous shape.
2026-05-14 23:11:06 +02:00
Marc-André Bombeck 8399c47874 refactor(api): adopt assertMedicationOwnership + rate-limit option-bag across 9 medication routes
Hoist the ownership-check boilerplate that nine medication sub-routes
were each carrying inline into the shared route-guard helper, and align
the three W19 routes that hand-wrote the rate-limit header loop on the
new apiError option-bag form.

Routes touched:
- titration: shared guard + replaced parseDoseString with the shared
  parseDoseMgOrNull + replaced the inline GLP1_DRUGS reverse-lookup
  with findDrugIdByBrand.
- side-effects: shared guard + option-bag headers + dropped the
  client-supplied 'category' field per code-M6 (server derives via
  categoryForEntry).
- inventory: shared guard + option-bag headers.
- cadence: shared guard layered on top of the W21 Fix-O timezone wiring.
- intake (POST + GET): shared guard.
- compliance: shared guard.
- phase-config (GET + PUT + DELETE): shared guard.
- api-endpoint (GET + PUT): shared guard.
- glp1: shared guard replacing Fix-K's inline copy + option-bag headers.

apiError now accepts { headers: Record<string,string> } in its third
argument bag; the headers flow through NextResponse and are stripped
from the meta envelope so the existing { errorCode, ... } meta usage
keeps its shape.
2026-05-14 23:09:50 +02:00
Marc-André Bombeck 2b712429c0 refactor(ui): adopt shared readError + ResearchModeStatus + design-system Select
- advanced-section: drops the local ResearchModeStatus interface in
  favour of the shared DTO, replaces the nested ternary that built the
  toggle's status caption with a researchModeStatusLabel helper
  threaded through the shared gate-state classifier.
- WelcomeCarousel / GoalsChipPicker / SourceCardGrid / BaselineForm:
  import the shared readError helper instead of the four hand-rolled
  copies — the request-failed fallback shape now evolves in one place.
- BaselineForm: swap the gender native <select> for the
  shadcn-radix Select component so the picker matches the rest of the
  form library (focus rings, keyboard nav, theme tokens).
2026-05-14 23:04:37 +02:00
Marc-André Bombeck d6456a30eb refactor(medications): extract MedicationDetailSection wrapper across 4 components
Lock the shared chrome (border + header row + dotted divider + body
band) into one wrapper so the three Wave-4b panels (Titration,
Scheduling, SideEffects) stop drifting on padding, border opacity,
and aria-wiring rules.

- src/components/medications/medication-detail-section.tsx (new) —
  composes the section shell with a stable titleId / headerExtras
  contract.
- TitrationSection — drops the hand-rolled templateFill helper (the
  i18n t() function already accepts the params bag); adopts the
  wrapper.
- SchedulingSection — adopts the wrapper; grows cadence-timeline
  cells to a 44 px tap-target per WCAG 2.5.5 while keeping the 12 px
  visible cell.
- SideEffectsSection — adopts the wrapper; swaps the native <select>
  for the shadcn-radix Select so the picker matches the rest of the
  form library.
- inventory-section — drops the local InventoryState string-union in
  favour of the Prisma MedicationInventoryState enum; replaces the
  reimplemented daysRemainingInUse with the widened pure helper.
- DrugLevelChart — adopts shared ResearchModeStatus,
  findDrugIdByBrand, and parseDoseMg (the local parseDoseMg is
  re-exported for test backwards-compat).
- inventory state-machine — widens daysRemainingInUse to accept
  either the full InventoryItemView or a thin { firstUseAt } shape.
2026-05-14 23:02:28 +02:00
Marc-André Bombeck c8ee4cb9fc feat(medications): shared helpers — readError + route-guards + dose-string + research-mode-types
Hoist five duplicated helpers into single source modules so each
contract evolves in exactly one place:

- src/lib/api/read-error.ts — replaces the four hand-rolled copies
  in the onboarding flow (Welcome / Goals / Source / Baseline).
- src/lib/medications/research-mode-types.ts — shared ResearchModeStatus
  DTO + tri-state gate helper consumed by both the DrugLevelChart
  and the Settings advanced section.
- src/lib/medications/route-guards.ts — assertMedicationOwnership
  for the eleven medication sub-routes, preserving the 404 leak
  shape across the privacy boundary.
- src/lib/medications/dose-string.ts — unified parseDoseMg + null
  variant replacing the DrugLevelChart and titration-route parsers.
- src/lib/medications/glp1-knowledge.ts — adds findDrugIdByBrand
  reverse lookup so the brand-to-id pattern stops being open-coded
  at every chart render.

28 new unit tests across the five helpers.
2026-05-14 22:54:25 +02:00
Marc-André Bombeck 5f5b8dfb49 chore(medications): delete unused research-mode-staleness module + add PR worker null-slot dup regression
`research-mode-staleness.ts` shipped in W19c-Safety as a sibling helper
for a future 90-day re-acknowledgment gate on the GLP-1 drug-level
chart. The wiring step never landed — the helper has zero production
importers (only its co-located test consumed it). The version-bump
re-prompt path in `/api/auth/me/research-mode/route.ts` already
covers the regulatory contract (a copy or EMA-EPAR ratchet forces
the dialog), so the calendar-staleness gate is intentionally deferred
to v1.4.26 and the unused module ships off-codebase rather than
sitting orphaned through the release.

Also pin the PR-detection worker's null-slot dedup contract against
a back-to-back invocation regression. The personal_records unique
index uses NULLS NOT DISTINCT on `(userId, metricType, metricSlot,
achievedAt)` — measurement-type PRs (slot = NULL) must coalesce
across consecutive worker invocations even when the candidate row
sits at the exact same achievedAt. A future refactor that drops the
NULLS-NOT-DISTINCT semantics would silently double-insert; this test
fails fast if that happens.

Fix-O / code-M5 + dead-H4 + senior-M3 + L2.
2026-05-14 22:43:53 +02:00
Marc-André Bombeck 2a65f2b179 feat(medications): thread timeZone into cadence + compliance helpers
The W19e pure cadence + compliance helpers used `Date.setHours`,
`getDay`, and `setHours(0, 0, 0, 0)` for every local-day boundary,
window-time application, and streak day-key. Those methods read the
host process's system time — every medication detail page on the
cadence route returned values relative to whatever zone the server
ran in (Europe/Berlin in prod, often UTC in CI), not the user's. A
Tokyo user reading the same data saw their 08:00 window land at
16:00 wall-clock and the day-of-week filter ran off the Berlin
weekday.

Thread an optional IANA `timeZone` argument through
`expandScheduleSlots`, `pairDoses`, `buildCadenceTimeline`,
`computeNextDose`, `missedDoses`, and `complianceChips`. Every local
operation now resolves through `Intl.DateTimeFormat`. When the
argument is omitted the helpers fall back to system-local — every
existing unit test (system-local by construction) keeps its
assertions green. The cadence route resolves the user's zone via
`resolveUserTimezone(user.id)` and forwards.

Regression tests pin the 08:00 wall-clock contract across Berlin /
Tokyo / Los Angeles and check that a "Mondays only" schedule lands
on each zone's local Monday.

Fix-O / code-H2.
2026-05-14 22:42:37 +02:00
Marc-André Bombeck 701fd5f251 fix(withings): anchor activity sync at noon UTC for tz-safe bucketing
Withings reports per-day aggregates with `date` as `YYYY-MM-DD`. The
v1.4.25 W17b writer anchored `measuredAt` at the day's 23:59:59 UTC so
the row would sort at the end of the day in chronological reads. That
shape mis-bucketed positive-offset users: a row from `date=2026-05-12`
arrived as 2026-05-13 08:59:59 in Tokyo, off-by-one for every
dashboard tile that reads the row in user-local time.

Anchor at noon UTC (`12:00:00Z`) instead — the standard "calendar-day
with no clock" representation per RFC 3339 §5.6. The instant lands
inside the local calendar day for every supported user timezone in
the practical [-11, +12) range (Honolulu through Tokyo), so the
analytics layer day-keys the row to the same calendar day every user
reads it in.

Regression test covers the four canonical user zones; the existing
end-of-day assertion is updated.

Fix-O / senior-M2.
2026-05-14 22:38:01 +02:00
Marc-André Bombeck 4c4fa645b1 docs(db): rewrite Migration 0057 PG11+ default-backfill comment
The original comment claimed that `DEFAULT 0` "only applies to *new*
inserts on PostgreSQL — existing rows are not backfilled". That's
wrong for PG11+: the fast-path `ADD COLUMN ... DEFAULT` materialises
the default for every existing row in O(1) time without rewriting the
table. The comment confused future readers about why the Wizard mount
treats `NULL` as 0 (defensive against the legacy ambiguity, not
because of any general PG behaviour).

Rewrite to match reality, and point at Migration 0060 as the follow-on
that flips the column to NOT NULL.

Fix-O / code-M8.
2026-05-14 22:36:33 +02:00
Marc-André Bombeck 3f7a8ce6a6 feat(db): Migration 0060 backfills onboardingStep + flips to NOT NULL
Migration 0057 shipped `onboarding_step INTEGER DEFAULT 0` as nullable so
the additive DDL avoided a long table rewrite during the W14b deploy.
The wizard mount code and the POST route then carried defensive
null-handling branches (read-side `?? 0`, write-side `OR step IS NULL`)
to keep legacy pre-W14b rows working. That left a tri-state surface the
route had to widen on every step-0 advance.

Backfill every NULL row to 0 (the same value the read-path already
substitutes), then `ALTER COLUMN ... SET NOT NULL`. schema.prisma drops
the `?` so the Prisma type-surface mirrors the DB. The conditional
update WHERE in `/api/onboarding/step` collapses to the strict-equality
form on every step — the legacy null branch becomes unreachable code
and is removed.

Fix-O / senior-M1.
2026-05-14 22:35:57 +02:00
Marc-André Bombeck a238ae48d1 fix(design): pin acknowledgment dialog footer + i18n back-button + drop GoalsChipPicker storage block
Three small surface fixes consolidated because each is a one-touch
change on a separate file:

- `ResearchModeAcknowledgmentDialog`: the entire `DialogContent`
  scrolled at `max-h-[90vh] overflow-y-auto`, which pushed the
  Acknowledge / Cancel CTAs below the fold on phone-class viewports.
  Move scrolling onto the body section only so the header and footer
  stay pinned.
- Medication history page back-button rendered the literal "Zurück"
  instead of going through `t()`. Wire the new `medications.back`
  key across all six locale bundles.
- `GoalsChipPicker` shipped a localStorage persistence block scoped
  by user id, but no consumer reads the same key. Drop the storage
  layer (and the unused `goalsStorageKey` /
  `ONBOARDING_GOALS_STORAGE_PREFIX` exports) until v1.4.26 lands the
  `User.onboardingGoals` column the picker is meant to feed.
2026-05-14 22:30:58 +02:00
Marc-André Bombeck 7c9b4b6563 fix(onboarding): conditional step write + 409 on concurrent advance
Two parallel tabs both reading `onboardingStep = 2` and both POSTing
`step: 3` would both succeed because the second write didn't re-check
the precondition — the wizard could double-advance and skip a step.
Move the write onto `updateMany` with the precondition embedded in
the WHERE clause: exactly one update lands, the loser sees count = 0
and returns 409 so the client can refresh and retry.

The WHERE clause covers `onboardingCompletedAt: null` so the same
guard catches a race with `POST /api/onboarding/complete`. The
`current === 0` branch widens the match to `step IS NULL OR step = 0`
so legacy pre-W14b accounts still advance from step 1.

Fix-M / code-M7.
2026-05-14 22:30:02 +02:00
Marc-André Bombeck 35cb488b6d fix(design): lift personal-record badge to WCAG AA contrast
The PR pill painted `text-dracula-green` (#50fa7b) on a
`bg-dracula-green/10` chip background. In dark mode that measured
around 2.6:1 — well under the 4.5:1 minimum WCAG 2.1 AA requires
for small text. Switch to the semantic `--success` token, which
resolves to a darker forest green on the light card (#14720a) and
to Dracula's neon green on the dark card, and lift the chip tint
from /10 to /15 for a sturdier edge. Both modes now clear AA.
2026-05-14 22:28:19 +02:00
Marc-André Bombeck 9ef1dd6e02 feat(api): legacy Withings webhook counter + migration URL in warning
The legacy ?secret=... route stays alive through v1.4.27 so existing
Withings subscriptions keep delivering while users re-subscribe. The
release-gate cannot tell when "legacy form usage has trended to zero"
because the route only emits a fire-and-forget Wide Event warning and
the warning text omits the destination URL anyone watching the access
log would need.

This pass adds:
- An in-memory `withings.webhook.legacy_form_total` counter scoped to
  the process (the right granularity for "since cold-start" usage).
- A `getLegacyFormTotal()` accessor + a test-only reset helper so the
  counter is observable without a backchannel.
- A `counters.withingsWebhookLegacyFormTotal` field on the existing
  `/api/admin/status` payload — the natural surface for ops counters
  alongside the user / measurement / worker blocks.
- The warning text now spells out `/api/withings/webhook/[token]` and
  the "re-subscribe with the path-segment token form" instruction.
- Two new assertions on the webhook test (counter increments on the
  query path, stays at zero on the header path, warning contains the
  migration URL) and one new assertion on the admin-status test.
2026-05-14 22:28:08 +02:00
Marc-André Bombeck 6f3b901c3e fix(workout): reject endedAt <= startedAt + zero-duration PR guard
A workout with `endedAt <= startedAt` produced a non-positive
`durationSec` that downstream consumers treated as a real zero —
worst case a fastest-5km PR of zero seconds, which would lock in
until manual cleanup. Add a `.superRefine` clause on
`createWorkoutSchema` so the ingest gate refuses the row.

The PR detector stays defensive against any pre-gate row or future
code path that bypasses the schema: MIN-direction slots now always
filter `durationSec > 0` regardless of which field carries the PR
value. A MIN slot sorts ascending, so the smallest value wins — a
zero-duration row would always be picked first.

Fix-M / code-M2.
2026-05-14 22:27:32 +02:00
Marc-André Bombeck 733fc2cec7 fix(design): align range-bar zone backgrounds with Dracula tokens
`<RangeBar>` painted its red / orange / green zones in raw Tailwind
palette (`bg-red-500/8`, `bg-yellow-500/12`, `bg-green-500/20`),
while the marker dot already drew from `var(--dracula-red|orange|green)`.
That split meant the zone backgrounds drifted out of the theme on
dark-mode and never tracked the rest of the token surface (PR
badges, alerts, status pills). Swap the zone tints to the matching
Dracula tokens and pin the absence of the legacy raw palette in the
existing extraction-contract test.
2026-05-14 22:27:30 +02:00
Marc-André Bombeck 4a7195a1bc fix(onboarding): 44px touch targets across wizard CTAs + carousel pager
Bring every back / skip / next / chevron / pager button on the
v1.4.25 onboarding wizard up to the 44x44 px WCAG 2.5.8 floor. The
previous `size="sm"` buttons measured 32 px tall, the chevron icons
were 32 px square, and each dot pager target was 10 px square — all
well under the target-size minimum for thumb-driven flows.

Carousel slides now derive their accessible name from the slide
title via `aria-labelledby`, so the polite live region (which
mirrors "Slide N of M") is the single source for position
announcements. The previous markup announced position twice on every
swipe: once from the slide's own `aria-label`, again from the
live region.
2026-05-14 22:26:46 +02:00
Marc-André Bombeck a400e5407b feat(api): tighten POST /api/medications/[id]/glp1 with Zod + audit + rate-limit
The GLP-1 convenience route shipped in W4d skipped the Zod / audit /
rate-limit triple the sibling inventory + side-effect POSTs ship with.
Hand-rolled `typeof === "number"` guards let NaN, Infinity, negative
doses, multi-megabyte notes, and 1970-era timestamps reach Prisma.

This pass adds:
- `glp1DoseChangePostSchema` + `glp1InventoryPostSchema` + the XOR
  `glp1PostBodySchema` in `src/lib/validations/medication.ts`.
- A 500-char cap on `note`, 200-char cap on `reason`, 100 mg cap on
  `doseValue`, integer cap on `delta`, and a +/-5-year window on
  `effectiveFrom`.
- The 30/min/user rate-limit copied from the sibling routes, keyed on
  `medication-glp1:post:<userId>`.
- `auditLog("medication.glp1.update", ...)` for every mutation, tagged
  with the affected row id and the dose / delta values.
- A test fixture mirroring the inventory + side-effect tests with
  thirteen assertions covering the new 422 paths, the rate-limit
  header, and both audit branches.
2026-05-14 22:26:14 +02:00
Marc-André Bombeck 1d5dcfb8b1 perf(inventory): bulk-update expired in-use items via updateMany
The daily cron sweep loaded every stale IN_USE row and issued one
`prisma.update` per row, recomputing the state in JS only to learn
what the selector already proved (state=IN_USE AND expiresAt < now ⇒
the row must transition to EXPIRED, because IN_USE implies
dosesRemaining > 0 and the expiresAt filter shows the deadline lapsed).
Collapse the round-trip onto a single `updateMany` so the cron's cost
is O(1) round-trips per user instead of O(stale rows).

Fix-M / code-H3.
2026-05-14 22:26:01 +02:00
Marc-André Bombeck 1b5906a504 fix(inventory): re-run state machine on markAsFirstUseAt PATCH
A back-dated first-use stamp whose 30-day in-use window has already
lapsed must land at EXPIRED, not IN_USE. The hand-rolled state
transitions in the PATCH route composed the next-row view but never
re-evaluated through the canonical state machine, so the route could
emit an IN_USE row whose expiresAt was already in the past. Wire
`computeInventoryState` over the composed view so every PATCH lands
on the same decision tree the intake hook and the daily expire cron
already share.

Test: ACTIVE pen + markAsFirstUseAt 45 days ago ⇒ EXPIRED.

Fix-M / code-H1.
2026-05-14 22:24:54 +02:00
Marc-André Bombeck 44e8229ff3 fix(logging): redact http.path + http.route in WideEventBuilder.setHttp
Closes the WITHINGS_WEBHOOK_SECRET leak (sec-C1): the api-handler
seeds http.path and http.route from url.pathname, which carries the
shared secret for /api/withings/webhook/<secret> requests. Routing
both fields through redactSecrets() at setHttp() time keeps the
secret out of stdout, the in-memory ring buffer, and Loki egress.
2026-05-14 22:24:42 +02:00
Marc-André Bombeck 8da6a4ae69 feat(logging): path-segment secret redaction via PATH_SECRET_PATHS registry
Adds a registry of URL path prefixes whose trailing segment is a
shared secret, seeded with the /api/withings/webhook/ entrypoint.
The redactSecrets() egress filter now rewrites those segments to
[REDACTED] before stdout, in-memory ring buffer, and Loki egress.
2026-05-14 22:23:49 +02:00
Marc-André Bombeck 4d5c735d18 fix(e2e): use canonical healthlog-locale cookie in locale-switch spec 2026-05-14 22:22:44 +02:00
Marc-André Bombeck c75a68a88c docs(planning): W21 reconcile plan 2026-05-14 22:20:35 +02:00
Marc-André Bombeck f88c275df4 docs(planning): W21 i18n runtime findings 2026-05-14 22:14:26 +02:00
Marc-André Bombeck 65319317cd docs(planning): W21 dead-code findings 2026-05-14 22:08:16 +02:00
Marc-André Bombeck e8139e9f93 docs(planning): W21 simplifier findings 2026-05-14 22:05:18 +02:00
Marc-André Bombeck 5448d19fee docs(planning): W21 code-review findings 2026-05-14 22:03:26 +02:00
Marc-André Bombeck 0f4431cbf1 docs(planning): W21 senior-dev findings 2026-05-14 22:02:38 +02:00
Marc-André Bombeck 94f97fa26a docs(planning): W21 product-lead assessment 2026-05-14 22:01:42 +02:00
Marc-André Bombeck 472da13da7 docs(planning): W21 design findings 2026-05-14 22:01:28 +02:00
Marc-André Bombeck 230e7531b0 docs(planning): W21 security findings 2026-05-14 22:01:16 +02:00