Commit Graph
1089 Commits
Author SHA1 Message Date
Marc-André Bombeck 5b8a47b4ff fix(insights): strip the duplicate briefing paragraph and rebalance the Health Score column
The /insights hero strip already renders briefing.paragraph as its
subtitle directly above the Daily Briefing card; the card then mounted
the same paragraph a second time inside its CardContent before the
key-findings list. The user read the same string twice within 200 px.

Drop the leading paragraph render from <DailyBriefing>; the card now
opens directly on the structured key-findings list (the part the hero
subtitle cannot surface). The empty-state branch keeps its 'Generate
briefing' CTA so the populated → empty flow remains intact.

Rebalance the Health Score card so it reads as a co-equal column of
the hero rather than a small inset:
  - lg:w-[260px] → lg:w-[360px] xl:w-[400px]
  - score number text-4xl → text-5xl sm:text-6xl
  - progress bar h-1.5 → h-2
  - inline Ask-the-Coach button retires; the hero strip's existing
    action covers it. onAskCoach stays on the prop signature so callers
    don't break.

Fold in BL-P4-9 L2: disclaimer text-[10px] → text-[11px] (the 10 px
size was borderline against the 12 px mobile floor flagged in the
W10 design review).

Tests updated: daily-briefing test asserts the paragraph slot is
absent in the populated branch; health-score-card tests pin the new
no-button render across English and German locales.
2026-05-15 16:48:03 +02:00
Marc-André Bombeck 25952d7d05 feat(admin): surface the carrier chip under the auth provider on the login overview
Adds a `carrierShortLabel()` helper in `_shared.tsx` that folds the
verbose GeoLite2 `autonomous_system_organization` strings down to
the glanceable DACH short labels Marc asked for: "Telekom",
"Vodafone", "1&1", "O2". Unknown organisations fall through to the
raw string so the chip stays informative.

`LoginOverviewSection` now renders the short label as a smaller
chip below the existing auth-provider chip, gated on
`entry.carrier !== null` so private-IP and offline-miss rows keep
the legacy single-chip layout.

The CSV export grows a matching `carrier` column between `location`
and `provider`. The API selector pulls the new `asn` + `carrier`
fields. New translation keys (`admin.carrier`, `admin.carrierUnknown`)
are referenced in code; bucket B6 lands the JSON entries.
2026-05-15 16:47:36 +02:00
Marc-André Bombeck fe18a8810c feat(coach): replace the textarea hint footer with an info-icon tooltip
The composer rendered the keyboard-shortcut hint ("Enter to send,
Shift+Enter for new line") as a prose `<span>` directly under the
textarea. Combined with the disclaimer and the send button, the
footer ate ~140 px of vertical room for what is essentially a single-
sentence affordance.

Replace the `<span>` with an `Info` icon wrapped in a Radix `<Tooltip>`
sat left of the send button. The existing `composerHint` translation
surfaces both as the trigger's `aria-label` (so screen-readers still
hear it on focus) and as the tooltip body for sighted users on hover.
The slot `data-slot="coach-input-hint"` stays so consumers can still
target the affordance.
2026-05-15 16:47:25 +02:00
Marc-André Bombeck 4d88764e4f fix(coach): collapse the evidence disclosure to closed-by-default
The assistant bubble's evidence `<details>` mounted open whenever the
user had `coachPrefs.showEvidenceByDefault = true`, which surfaced the
raw measurement values (e.g. "Blutdruck letzte 30 Tage: 226") under
every reply. The disclosure is now always collapsed; the user expands
by click. The persisted pref stays in the schema for backward
compatibility but no UI reads or writes it, so the value is dead-code-
tolerated for one release and can be dropped in the next migration.

Drops the `evidenceDefaultOpen` prop wiring from `<MessageThread>` and
the corresponding `<Switch>` in the Coach settings sheet. Adjusts the
sheet test that previously asserted the toggle was rendered.
2026-05-15 16:45:55 +02:00
Marc-André Bombeck 053d678ac7 refactor(dashboard): promote GLP-1 schedule dates to a header pill row and drop the green seam
The faint dracula-green left-seam on the outer wrapper was decoration
without a semantic tie to the schedule rows beside it. The Syringe-icon
in the title row already carries the 'active therapy' signal at a
glance.

Replace the <dl> grid with a soft-green pill bar that groups Last- and
Next-Injection as one cohesive 'therapy schedule' unit. Each pill keeps
its data-slot anchor (glp1-tile-last / glp1-tile-next) so existing
tests stay in lockstep; new test asserts the data-slot='glp1-tile-schedule'
wrapper renders and the border-l-dracula-green/60 + border-l-2 classes
no longer appear on the outer wrapper.
2026-05-15 16:44:39 +02:00
Marc-André Bombeck 1d8b80698e feat(audit): wire ASN+carrier into the audit-log create/update path
Completes the v1.4.27 B3 audit-log work that landed in e8fb0a75. The
prior commit shipped the backfill helper but the schema + migration
+ audit.ts write-path were dropped during a concurrent staging
window. This commit puts them back.

`prisma/schema.prisma` grows two nullable columns on AuditLog (`asn`
+ `carrier`), the matching idempotent `IF NOT EXISTS`-guarded
migration lands at `0061_audit_log_carrier`, and the auditLog helper
resolves location + ASN row in parallel (single 3 s race, single
`prisma.auditLog.update` carries whichever fields the resolvers
filled).

Test coverage: five new cases on audit.test.ts cover all four
combinations of location/ASN hit-or-miss plus a row with no
carrier organisation.
2026-05-15 16:43:48 +02:00
Marc-André Bombeck c0ec1fecfb feat(dashboard): wire the GLP-1 drug-level pane behind a tab strip and range picker
The GLP-1 tile previously rendered a single weight chart — a downstream
proxy for the dose-response. The maintainer-flagged headline is the
estimated drug-level curve because it surfaces the sawtooth shape of
the weekly cycle directly.

Add a two-tab segmented control (Drug-Level default / Weight) inside the
chart pane and a 7d / 30d / 90d / All range strip above. The Drug-Level
tab mounts <DrugLevelChart compact …>, which now drops its outer card
wrapper and renders at 160 px instead of 240 px when 'compact' is set,
so the dashboard tile owns the card surface. Range selections thread
through a new windowHoursBefore prop on DrugLevelChart; the sample step
scales 6/12/24/48 h with window length so 'All' mode does not paint
thousands of points. The standalone surface at /medications/[id]/history
keeps its full 21-day default and full-card chrome — both consumers
share one implementation.

New translation keys referenced (B6 sweep picks up):
- dashboard.glp1.tabLevel
- dashboard.glp1.tabWeight
- dashboard.glp1.tabsAria
- dashboard.glp1.rangeStripLabel
- dashboard.glp1.levelUnavailable
- dashboard.glp1.weightUnavailable

Test coverage extended: glp1-tile test asserts tab strip + range strip
DOM, drug-level default selection, level-unavailable hint when the
medication has no id; the existing DrugLevelChart test suite continues
to pin the standalone-surface contract.
2026-05-15 16:43:24 +02:00
Marc-André Bombeck e8fb0a7521 feat(audit): persist ASN and carrier on login audit rows
Adds two nullable columns to AuditLog (`asn Int?` + `carrier String?`)
via an additive `IF NOT EXISTS`-guarded migration. The audit-log
write path now resolves the location and the ASN row in parallel and
writes whichever fields the resolvers could fill in a single update.

A new `runGeoBackfill` helper walks rows where `location IS NULL`
AND `ipAddress IS NOT NULL` AND `createdAt > now() - 30 days`,
capped at 5 000 rows per pass. Useful for picking up rows that
landed during an `ipwho.is` outage or before the GeoLite2 MMDBs
were bundled into the image.
2026-05-15 16:43:03 +02:00
Marc-André Bombeck 3e930e68f6 feat(notifications): localise admin and user Telegram messages to the recipient locale
Several call-sites composed notification bodies in English or hardcoded
German without consulting User.locale. Swap each surface to a
translator-aware path:

- src/app/api/internal/deploy-webhook/route.ts uses
  dispatchLocalisedNotification per admin, reading
  notifications.admin.deployFailedTitle and
  notifications.admin.deployFailedBody with param substitution.

- src/app/api/admin/notifications/test/route.ts resolves the admin
  locale once at the top of the handler, then composes the test
  notification via getServerTranslator. This route talks to the
  senders directly because it needs per-channel success/error
  feedback for the UI, so it stays off the dispatcher.

- src/app/api/settings/telegram/test/route.ts has the same shape for
  the per-user path; it reads the user locale and translates
  notifications.user.telegramTestBody before sending.

- src/app/api/admin/notifications/reminder-check/route.ts uses
  dispatchLocalisedNotification with per-row recipient locale, so the
  affected user (not the admin running the check) sees the message
  in their language.

Reminder-worker is already localised via getPhaseMessage; no change.
src/lib/integrations/status.ts is out of scope and left for a later
symmetry-sweep.

Tests:
- src/app/api/settings/__tests__/telegram-test-locale.test.ts covers
  de, fr, null, unsupported locale paths plus rate-limit,
  missing-token, and send-failed branches.
- src/app/api/internal/deploy-webhook/__tests__/route.test.ts swaps
  its admin-alert assertion from dispatchNotification to
  dispatchLocalisedNotification.

New translation keys (B6 picks up the JSON edits):
- notifications.admin.deployFailedTitle
- notifications.admin.deployFailedBody
- notifications.admin.testNotificationTitle
- notifications.admin.testNotificationBody
- notifications.admin.reminderCheckMissedTitle
- notifications.admin.reminderCheckMissedBody
- notifications.admin.reminderCheckOverdueTitle
- notifications.admin.reminderCheckOverdueBody
- notifications.user.telegramTestBody
2026-05-15 16:42:24 +02:00
Marc-André Bombeck ef50b83746 docs(planning): seed v1.4.28 backlog with F7 deferral note 2026-05-15 16:38:22 +02:00
Marc-André Bombeck 9afb31bd15 chore(dashboard): retire the standalone InsightsCardPreview surface
The dashboard preview duplicated the much-richer /insights advisor surface
that already lives one click away (Daily Briefing, hero strip, correlation
row, full advisor, weekly-report banner). The preview also competed
visually with the GLP-1 tile for the same vertical real estate above the
chart row.

Drop the import + render + isChartVisible gate in src/app/page.tsx, drop
the insightsPreview widget entry from src/lib/dashboard-layout.ts, drop
the matching row in src/components/settings/dashboard-layout-section.tsx,
and delete src/components/insights/insights-card.tsx and its test file.
The advisor query hook stays on /insights — the dashboard no longer
mounts it. Update the dashboard-layout contract test to assert the
widget id is absent.

F7 weekly-report scan: ran grep -rinE 'Wochenreport|weekly report|/insights/report' across src/ and messages/. All weekly-report click affordances on /insights are correctly wired (HeroStrip Generate button → currentWeekHref, WeeklyReportBanner → /insights/report/[week], InsightAdvisorCard banner → /insights/report/[week]). The /insights/report/[week] route mounts WeeklyReportView and renders the full surface. No dead click affordance was located inside the 30-minute scan budget. The retired InsightsCardPreview was the only dashboard-anchored insight card with a CTA — its removal in this commit is the most likely reason the maintainer perceived a dead click. F7 deferred to v1.4.28 with a maintainer-screenshot ask; recorded in .planning/v1428-backlog.md.

i18n keys still referenced after this change (B6 sweep will reconcile):
none from this commit. Keys 'dashboard.insightsPreview' and 'insights.aiInsights' are now unreferenced in code and become candidates for B6's dead-key sweep.
2026-05-15 16:38:22 +02:00
Marc-André Bombeck 50150e35ef feat(geo): bundle GeoLite2-City and add an offline-first lookup path
Rewrites src/lib/geo.ts so lookupIpLocation reads from the bundled
MaxMind GeoLite2-City MMDB first and falls back to the existing
ipwho.is path only for IPs the offline DB cannot resolve. Adds a
sibling lookupIpAsn that returns { asn, carrier } from the
GeoLite2-ASN MMDB so the admin login-overview can grow a carrier
chip alongside the auth-provider chip.

The MMDB files are too large to vendor in git. scripts/fetch-geolite2.sh
downloads them at build time using a MaxMind license key; the
Dockerfile copies assets/geolite2/ into /opt/geolite2/. When the
license key is absent or the files are missing the image still
builds and the resolver falls back to ipwho.is (v1.4.26 behaviour).

GeoLite2 is CC BY-SA 4.0 — attribution lives on the new public
/about page and in docs/audit/v1427-summary.md.
2026-05-15 16:36:55 +02:00
Marc-André Bombeck 2c9d17c333 feat(notifications): add a translator-aware dispatch helper
The base dispatchNotification is locale-agnostic by design: it
forwards title and message to every sender verbatim. Several
call-sites (admin alerts, deploy webhook, Telegram test routes,
reminder-check) compose bodies in English without consulting
User.locale, leaving recipients with mismatched copy.

Introduce dispatchLocalisedNotification next to the base dispatcher.
The helper resolves the recipient's User.locale (falls back to the
project default when missing, unsupported, or unreadable), calls
getServerTranslator(locale).t on both the title and message keys
with optional params, and delegates to dispatchNotification with the
composed strings. Missing translation keys fall back to the raw key
and log a wide-event warning so ops can spot bundle drift.

Locale resolution is intentionally restricted to the persisted
User.locale column. The helper runs from background jobs and webhook
handlers where the request context may not belong to the recipient
(an admin alert resolves the admin's locale, not the affected user's),
so cookies and Accept-Language are out of scope.

Forward-compat scaffolding: an optional channel selector
('telegram' | 'email') is accepted and forwarded as
metadata.preferredChannel so call-sites can declare intent today;
the dispatcher continues to fan across every enabled channel until a
follow-up wires routing.

Tests:
- notifications/__tests__/admin-locale.test.ts drives the helper
  through de / fr / null / unsupported / missing-row locale paths,
  the missing-translation-key fallback, the event-type and metadata
  pass-through, and a Prisma-throws path that must not propagate.

Bundle keys referenced by this commit (B6 will land the JSON edits):
- notifications.admin.*
- notifications.user.*
2026-05-15 16:36:19 +02:00
Marc-André Bombeck 1a8e75989a docs(planning): bucket B2 implementation report for v1.4.27
Lists the four B2 commits, the scope of each, the translation-key
plumbing left for the localisation pass, why the optional Playwright
timing test was skipped, and the deviations from the original
fix-plan dispatch.
2026-05-15 16:32:56 +02:00
Marc-André Bombeck 9c8660a4e0 feat(settings): replace single-spinner loading with skeleton rows on Thresholds and Sources
Replaces the centred `<Loader2 /> Loading` line in
`<ThresholdsEditorSection>` and `<SourcesSection>` with a skeleton
list whose row count matches the loaded UI (14 rows each — one per
`METRIC_ORDER` entry and one per `SOURCE_PRIORITY_METRIC_KEYS` entry
respectively). The skeleton rows reuse the live UI's container
classes (`border-border space-y-3 rounded-lg border p-4` on
Thresholds; `border-border bg-background/30 space-y-2 rounded-md
border p-3` on Sources) so the post-fetch swap lands in place. The
`<Skeleton>` primitive's `motion-reduce:animate-none` covers the
reduced-motion preference. Combined with the `min-h` shell reserve
from the previous commit, this removes the page-height jump that
landed under the sticky sidebar when these two sections finished
loading.
2026-05-15 16:31:42 +02:00
Marc-André Bombeck e060ab33a2 fix(integrations): silence the Withings scope-skip path from the 3-strike alert ladder
The v1.4.26 scope-skip short-circuit in `sync-activity.ts` and
`sync-sleep.ts` called `recordSyncFailure` for a deliberate no-op,
which incremented `IntegrationStatus.consecutiveFailures` and tripped
the 3-strike admin Telegram alert for every legacy connection. Webhook
deliveries (appli=16 / appli=44) could run the counter from 0 to 3 in
seconds on a freshly-deployed v1.4.26 install, and pre-existing
counters that survived the deploy (e.g. an 87-strike row from the
pre-fix transient burst) paged immediately on the first scope-skip
after the 24h re-alert window closed.

Extract a new `parkIntegrationAtReauth` helper next to
`recordSyncFailure` that sets the row to `error_reauth` without
incrementing the counter, without writing an `integrations.sync.failed`
audit row, and without entering the threshold ladder. A standalone
`integrations.reauth_required` audit row is written on the first park
so the operations trail still shows the event; re-parking the same
scope-skip is idempotent (no extra audit write). Swap the two
deliberate scope-skip call-sites in `withings/sync-activity.ts` and
`withings/sync-sleep.ts` to the new helper.

The defence-in-depth 403 catch-block in each sync routine stays on
`recordSyncFailure` — a 403 reaching the catch IS genuinely unexpected
once the scope-skip lands above, so the alert ladder should still fire
on that asymmetric pair. BL-P3-2 parity verified between activity and
sleep.

Tests extended:
- `integrations/__tests__/admin-alert.test.ts` adds a
  `parkIntegrationAtReauth` block asserting state=error_reauth without
  counter increment, no `dispatchNotification`, and audit-log
  idempotency on a second call.
- `withings/__tests__/sync-activity.test.ts` +
  `withings/__tests__/sync-sleep.test.ts` swap their scope-skip
  assertions to `parkIntegrationAtReauth` and keep the 403 catch-block
  on `recordSyncFailure` to prove the defence-in-depth alert path
  survives.
2026-05-15 16:30:56 +02:00
Marc-André Bombeck 3ed78a032b fix(charts): align MoodChart height with the rest of the trend strip
MoodChart rendered at 280 px while every sibling chart on the dashboard
card row (weight, BP, pulse, body fat, sleep, steps, medication
compliance) renders at 240 px. The 40-px outlier broke vertical rhythm
when scrolling past the trend strip — the row swelled when it hit the
mood card.

Consume CHART_HEIGHT_PX from the shared constants module so the empty
state and the populated chart now stride with the rest of the row. Bump
the populated-chart override from 280 to 240 in the same edit.
2026-05-15 16:30:18 +02:00
Marc-André Bombeck 1e1e7e133e docs(planning): iOS handoff addendum for standalone usage and server pairing 2026-05-15 16:30:02 +02:00
Marc-André Bombeck b280253033 fix(settings): reserve a minimum main-column height in the settings and admin shells
Adds `min-h-[calc(100dvh-12rem)]` to the `<main>` element in both
`<SettingsShell>` and `<AdminShell>`. The sticky 220 px sidebar is
anchored to the page scroll; previously the grid's row height tracked
the section's content height, so swapping a short spinner for a long
list (Thresholds: 14 rows; Sources: 14 metric tiles) jumped the page
height under the sidebar and shifted the active link off-screen. The
12 rem reserve covers the global header (5 rem) + section header
(~5 rem) + a small buffer, so the spinner-to-content transition no
longer changes the viewport height. Short pages (`/settings/about`)
still render short because `min-h` does not clamp tall content.
2026-05-15 16:29:52 +02:00
Marc-André Bombeck d567e45423 feat(charts): extract shared chart-height and range-preset constants
New module src/lib/charts/constants.ts exports CHART_HEIGHT_PX (240),
CHART_MINI_HEIGHT_PX (160), and CHART_RANGE_PRESETS (7d / 30d / 90d / All).
The constants give the dashboard chart strip and the GLP-1 tile one
source of truth so future retuning lands in one place.
2026-05-15 16:27:16 +02:00
Marc-André Bombeck 2e4edbf2d0 fix(settings): raise the TimezonePicker inner gap to match the form rhythm
The picker row sits between two `space-y-4` (16 px) form rows, so the
old `gap-2` (8 px) between the select and the detect button read as a
tighter rhythm than the rest of the profile form. `gap-3` (12 px) keeps
the detect button visually attached to the select (it is a hugged
action) while no longer breaking the cadence the surrounding rows set.
2026-05-15 16:27:01 +02:00
Marc-André Bombeck a04e119cac fix(settings): pair date-of-birth with language in one grid row
Moves the language select into the same `grid sm:grid-cols-2` row as
date of birth and drops the standalone language wrapper plus its
`sm:max-w-xs` clamp. The profile form now carries a uniform two-cell
rhythm from top to bottom instead of alternating between paired and
single-cell rows. Updates the mobile-consistency e2e assertion to match
the new contract: dob + language MUST share a grid row.
2026-05-15 16:26:26 +02:00
Marc-André Bombeck 0f5369131e docs(planning): v1.4.27 session handoff with R2 actuals and scope-maximization directive 2026-05-15 16:18:57 +02:00
Marc-André Bombeck e0d6bba304 docs(planning): v1.4.27 fix plan — consolidated R1 triage
Consolidates the six R1 audit reports (R1.1 dashboard, R1.2 settings/admin,
R1.3 coach/insights, R1.4 notifications, R1.5 iOS offline-first research,
R1.6 backlog sweep) plus the 17 backlog items pulled forward into a single
fix-surface plan with seven touch-disjoint buckets, a file-touch collision
matrix with zero same-line collisions and four sequenced edits, and a
Round 3 dispatch sequence (5 parallel + 2 sequenced). Decides /api/audit-log
to DELETE as an orphan endpoint, parks all four iOS server-side preps
(syncVersion, deletedAt, GET /api/sync/state, ETag) to v1.4.28, and adds
22-offline-first-architecture.md to the iOS handoff doc-pack carrying R1.5's
research forward for the iOS-side reader.
2026-05-15 16:16:00 +02:00
Marc-André Bombeck d16a627d74 docs(planning): R1.6 v1.4.26 backlog sweep for v1.4.27 inclusion 2026-05-15 16:08:25 +02:00
Marc-André Bombeck 7a9cf46d54 docs(planning): R1.4 notifications and Withings classification audit 2026-05-15 16:07:58 +02:00
Marc-André Bombeck c29cd88b29 docs(planning): R1.1 dashboard UX audit and GLP-1 secondary tile spec 2026-05-15 16:07:42 +02:00
Marc-André Bombeck 43a91f2ffa docs(planning): R1.3 coach and insights data-driven audit 2026-05-15 16:06:51 +02:00
Marc-André Bombeck e6c8e22d14 docs(planning): R1.5 iOS offline-first and sync research 2026-05-15 16:06:44 +02:00
Marc-André Bombeck a547424062 docs(planning): R1.2 settings and admin UX audit 2026-05-15 16:06:12 +02:00
Marc-André Bombeck c74a0c9845 docs(planning): v1.4.27 plan — comprehensive QoS pass and backlog sweep 2026-05-15 16:00:49 +02:00
Marc-André Bombeck ae244cab12 chore: merge v1.4.26 into develop
# Conflicts:
#	CHANGELOG.md
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/pl.json
#	package.json
#	src/lib/withings/__tests__/sync-activity.test.ts
#	src/lib/withings/__tests__/sync-sleep.test.ts
#	src/lib/withings/sync-activity.ts
#	src/lib/withings/sync-sleep.ts
2026-05-15 07:38:29 +02:00
Marc-André Bombeck d98e85cd03 chore(release): v1.4.26 — privacy policy for App Store submission
Public privacy-policy page at /privacy enumerates every Apple
HealthKit identifier the iOS app reads, names every third-party
sub-processor, and restates the EU MDR + GDPR boundaries. Unblocks
Apple BLOCKER 2 of the iOS App Store Connect submission by giving
the registered Privacy URL a real page to point at.
v1.4.26
2026-05-15 07:35:05 +02:00
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 17b8d8d2c2 chore(release): v1.4.25
Largest feature delta in the v1.4.x line. Insights expands into seven dedicated metric sub-pages; GLP-1 medication tracking lands end-to-end (drug-level chart with Research Mode acknowledgment, EMA titration ladder, cadence + compliance, side-effect taxonomy, pen/vial inventory); cross-source priority becomes a two-axis resolver; per-user timezone threads through ten analytics surfaces; Withings activity + sleep v2 sync; onboarding rebuilt as a nested-route wizard; Personal Records ship end-to-end; Health Score provenance accordion; Coach runs native first-party prompts across six locales with a 1800-assertion refusal-probe matrix; OpenAPI drift gate hard-fails; multi-arch GHCR image. Migrations 0043 through 0060 are additive and forward-only. PROMPT_VERSION 4.24.0 → 4.25.0 with GROUND RULE 9 (refuses dose recommendations) and GROUND RULE 15 (refuses drug-level estimates with MDR + MDCG 2021-24 cites).

See CHANGELOG.md for the full per-section delta.
v1.4.25
2026-05-14 23:49:32 +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