Commit Graph
24 Commits
Author SHA1 Message Date
Marc-André Bombeck cf8e702298 i18n: translate duplicate-timestamp and sleep description in fr/es/it/pl
`measurements.duplicateTimestamp` (the v1.4.28 row-edit banner) and
`insights.sleep.description` (the schlaf sub-page header) both
shipped with English copy in the four non-DE/EN bundles. Replace
with native sentences so a French, Spanish, Italian, or Polish user
sees their own locale on the surfaces this release touched.
2026-05-16 02:37:44 +02:00
Marc-André Bombeck 75773ca07a i18n: add the lastYear coach window key 2026-05-16 02:05:42 +02:00
Marc-André Bombeck 8808561581 fix(medications): shorten side-effects add CTA across locales
Trims the side-effects add-CTA in all six locales to a single verb so
the chip stops overflowing the card on narrow viewports. The section
title already carries the context. Pairs with the row-layout adjustment
that lifted the date stamp into a fixed-width column and line-clamped
the free-text note.

Closes the i18n half of FB-F1; the test contract for the new chip text
was pinned in the prior commit.
2026-05-16 01:50:23 +02:00
Marc-André Bombeck 9a020f2122 feat(insights): explain the HealthScore delta on tap
The "vs last week" delta line on the HealthScore card has been
shipping a digit without explaining what the comparison covers, why
the digit moved, or what the user can do next. FB-I1 — Marc asked
for a tappable `?` glyph next to the delta line that opens a short
read explaining the three things in one shot.

The new `<HealthScoreDeltaExplainer>` mounts a single icon-only
`HelpCircle` button next to the existing delta caption. Tapping the
glyph opens a popover on `md+` and a bottom-sheet on phone-class
viewports — the existing `<ResponsiveSheet>` primitive handles the
branch, mirroring the same pattern landed in v1.4.27 MB1.

Body copy is a three-sentence Marc-Voice read: which components
contributed, what window the comparison covers, what concrete next
step the user can take. Six locales ship native translations — no
English fallback strings on the new keys.

The explainer only mounts when the parent surfaces a numeric delta;
the "no history yet" branch keeps the existing caption untouched
because the explainer would have nothing to compare against.

Tests pin the trigger button's `data-slot` and locale-grounded
accessible label across EN/DE, lock in the "closed by default" SSR
snapshot, and verify the body copy ships in every locale via the
existing `t()` payload. The interactive open behaviour is exercised
on the live page; the SSR contract is enough to catch a future
silent locale drop.
2026-05-16 01:50:04 +02:00
Marc-André Bombeck 8c8d6dc27f chore(medications): drop the Bestand section from GLP-1 detail
Retire the entire inventory surface from the GLP-1 medication card:
the inline "pens left · weeks of supply" summary line, the disclosure
that wrapped the per-pen list, and the InventorySection component
file (no other consumer). The orphan i18n keys under
medications.inventory.* and the glp1Inventory* labels retire alongside
the help-text revision for glp1DosesPerUnitHelp. The iOS-consumed
Glp1InventoryDTO slot on /api/medications/[id]/glp1 stays byte-stable
in the response shape, and the server-side state-machine helpers
remain for that endpoint's computed weeksOfSupply.
2026-05-16 01:33:50 +02:00
Marc-André Bombeck 8c81af10d6 chore(medications): drop the Dosis-Historie disclosure from GLP-1 detail
Retire the inline dose-history `<details>` block on the GLP-1
medication card and the IntakeHistoryList mount on the standalone
medication detail page. The IntakeHistoryList component had no other
consumer, so the file retires together with its orphan i18n keys.
The iOS-consumed GET /api/medications/[id]/intake endpoint, its Zod
schema and the doseChanges wire field on /api/medications/[id]/glp1
all stay byte-stable.
2026-05-16 01:27:40 +02:00
Marc-André Bombeck 52edf85f02 chore(insights): retire the InsightAdvisorCard surface
Drop the InsightAdvisorCard component, its four integration tests
under src/components/insights/__tests__, the page-level mount on
/insights, the orphan i18n keys (advisorTitle, aiOverviewTitle), the
e2e advisor-card spec, the dashboard.spec.ts advisor assertion and
every related comment. The InsightsTabStrip regenerate icon stays
untouched as the sole power-user refresh affordance. The iOS-consumed
POST /api/insights/generate endpoint and its request/response shape
stay byte-stable.
2026-05-16 01:19:14 +02:00
Marc-André Bombeck cad53a68a3 chore(insights): retire the weekly-report surface
Drop the entire weekly-report path: the /insights/report/[week] route,
the WeeklyReportView component, the hero strip's banner mount and
"Wochenbericht erstellen" action button, the ISO-week helpers, the
schema slot, the AI prompt rule + OUTPUT FORMAT block, the openapi
field, the i18n keys and every associated test. Coach now owns the
sole hero-row action. iOS contract is unaffected — no native consumer
reads any weekly-report endpoint or schema slot per the v1.4.28 iOS
contract inventory.
2026-05-16 01:14:26 +02:00
Marc-André Bombeck 8e5f71b1b9 chore(dashboard): retire the GLP-1 tile
Drop the standalone GLP-1 dashboard tile and its data-fetch route.
The /api/dashboard/glp1 endpoint has no consumer outside the retired
tile (no native client reads it), so the route, the tile component,
its test, the e2e spec, the locale keys, the query-key entry and the
drift-guard assertion all retire together.

The medication detail page and its bidirectional contracts stay
untouched. An opt-in dashboard widget that surfaces the same level
chart will land later in this cycle, anchored on the existing
dashboard-layout toggle list.
2026-05-16 01:00:09 +02:00
Marc-André Bombeck 59ef95f284 fix(dashboard): align BD-Zielbereich tile with shared TrendCard primitive
The BD-Zielbereich tile rendered "1.1." instead of the expected
percent. Root cause: the dashboard mount synthesised a fake
`slope30 = bpTrendDelta / 30` so the trend arrow could move. The
result was a small float (≈ 1.1 for a 33-point delta) that the
TrendCard's downstream formatter pipeline rendered as a
date-shaped artefact — the maintainer's screenshot regression
across v1.4.22 / v1.4.25 / v1.4.26 patches that kept treating the
symptom instead of the divergence.

Rewrite the tile against the shared `<TrendCard>` primitive with
the same prop contract every other tile uses (`latest`, `avg7`,
`avg30`, `slope30=null`, `trend7Delta`, `compareBaseline`,
`compareDelta`). Drop the synthetic-slope block, drop the
`avgAllTime` prop family from `<TrendCard>` (the BD tile was the
only consumer), drop the orphan `charts.avgAllTimeShort` i18n key
across six locales. The all-time aggregate stays visible on the
`/targets` Blutdruck card with full context.

New regression spec at `src/components/charts/__tests__/bd-tile.test.tsx`
locks the BD tile's headline to a numeric percent string, asserts
DOM-shape parity with the Weight tile, and pins the absence of the
retired `trend-card-all-time` slot.

iOS contract: none. `/api/analytics` still emits the
`bpInTargetPct*` family unchanged (the `bpInTargetPctAllTime`
field stays for the /targets page consumer); iOS does not consume
the dashboard tile.
2026-05-16 00:35:28 +02:00
Marc-André Bombeck 538b44f74d fix(api): return 409 on duplicate-timestamp measurement edit
The PUT /api/measurements/[id] handler dispatched the Prisma
update without a try/catch, so re-pointing measuredAt onto an
existing (userId, type, measuredAt, source, sleepStage) tuple
bubbled the P2002 violation as a 500 and the row-edit Sheet
rendered the generic save-error toast.

Mirror the POST handler's P2002 catch: return a clean 409 with a
stable `meta.errorCode = "measurement.duplicate_timestamp"` and a
new measurements.duplicateTimestamp i18n key across the six
locales. The mutation in measurement-list.tsx reads the error
code from the response envelope and surfaces the localised
banner instead of the upstream English fallback.

iOS contract — PATCH /api/measurements/{id}: additive. The 200
happy-path response shape (MeasurementWireDTO), the 422
validation path and the updateMeasurementSchema are untouched.
The new 409 path is strictly clearer than the 500 it replaces.
2026-05-16 00:15:47 +02:00
Marc-André Bombeck 82a23069a5 feat(admin): surface the GeoLite2 availability state on the system status page
Both the `/admin` overview snapshot and the full `/admin/system-status`
page now read `offlineGeoEnabled` from `/api/version` and render a
status row with a green chip when the offline databases are loaded or
a yellow chip with the configuration hint when the runtime is on the
ipwho.is fallback. The row only renders when the field is present so
legacy `/api/version` responses do not produce a placeholder slot.

Adds six locale keys per bundle (`admin.offlineGeoLabel`,
`offlineGeoEnabled`, `offlineGeoFallback` plus the three
`admin.overview.snapshotOfflineGeo*` siblings) across all six
locales.
2026-05-15 22:21:03 +02:00
Marc-André Bombeck 53c9e43795 feat(geo): send a one-shot admin notification when the offline databases are missing
The runtime gate for the resilient GeoLite2 fallback. On the first
`lookupIpLocation` or `lookupIpAsn` call that has to leave the offline
tier, the resolver checks `offlineGeoReady()` — false if the geo asset
directory carries the `.empty` marker or the City MMDB is absent — and
fires `dispatchLocalisedNotification` to every admin user, exactly
once per process lifetime, with a body that points the maintainer at
the GitHub Actions secrets page where `MAXMIND_LICENSE_KEY` belongs.

Adds the two translation keys
`notifications.admin.offlineGeoUnavailableTitle` /
`offlineGeoUnavailableBody` to every locale bundle, locks them down in
the drift-guard, and covers the latch, the marker, the no-admin path
and the ASN fallback with 8 new vitest cases under
`geo-offline-detection.test.ts`.
2026-05-15 22:15:48 +02:00
Marc-André Bombeck 9266e405ca fix(i18n): translate the GLP-1 dashboard tile copy across FR/ES/IT/PL
`dashboard.glp1.lastInjection`, `nextInjection`, `weightDelta`, and
`inDays` were translated to DE only in the v1.4.27 dashboard rebuild
— the FR/ES/IT/PL bundles still held the EN strings verbatim and
rendered "Last injection" / "Next injection" / "since start" / "in
{count} days" on the live tile. Translate all four keys into native
phrasings:

- FR: `Dernière injection` / `Prochaine injection` / `depuis le début` /
  `dans {count} jours`
- ES: `Última inyección` / `Próxima inyección` / `desde el inicio` /
  `en {count} días`
- IT: `Ultima iniezione` / `Prossima iniezione` / `dall'inizio` /
  `tra {count} giorni`
- PL: `Ostatnia iniekcja` / `Następna iniekcja` / `od początku` /
  `za {count} dni`

The other seven keys in the `dashboard.glp1.*` namespace already ship
correctly across every locale, so this closes a copy-paste residue
rather than a missing namespace.
2026-05-15 21:04:34 +02:00
Marc-André Bombeck 815f31a321 fix(i18n): restore interpolation spacing on pagination and counter keys across FR/ES/IT/PL
The earlier translation pass dropped the surrounding spaces around the
connector word on ten counter / pagination keys — the result was
`Page 2 de10` / `Page 2 z10` rendering on every FR/ES/IT/PL
`pageInfo`-shaped surface plus the admin pagination, onboarding tour
step pill, getting-started progress chip, target consistency chips,
and target weekly summary headline. Restore the spaces and translate
the surrounding nouns natively at the same pass:

- pageInfo (measurements / mood / medications), `admin.section.auditLog.pageOf`
  → FR `Page {page} sur {total}`, ES `Página {page} de {total}`,
  IT `Pagina {page} di {total}`, PL `Strona {page} z {total}`.
- `admin.showingEntries`, `onboarding.tour.stepOf`, `gettingStarted.progress`,
  `targets.consistency.daysInRange`, `targets.consistency.daysLogged`,
  `targets.summary.weekTitle` — translated in full with native verb forms
  and balanced placeholder whitespace.

The drift-guard test stays green; a follow-up tightening to assert
`[A-Za-zÀ-ÿ]\{|\}[A-Za-zÀ-ÿ]` returns no match across the bundle would
have caught the original drift and is queued for the next i18n round.
2026-05-15 21:03:32 +02:00
Marc-André Bombeck 7850976b99 chore(i18n): replace remaining assistant-product references with neutral terms across six locales
Rewrite two strings that still leaked the forbidden assistant-product
vocabulary across every locale bundle:

- `account.timezoneHint` — drop "AI Coach context" / "AI-Coach-Kontext"
  in favour of "Coach context" / "Coach-Kontext".
- `i18n.maintainershipBanner.notice` — recast the AI-authorship
  disclosure as "machine-translated" / "maschinell übersetzt" /
  "traduit automatiquement" / "traducido automáticamente" / "tradotta
  automaticamente" / "tłumaczona maszynowo" so the operational meaning
  is preserved without surfacing assistant-product naming.

Document the two exempt-by-necessity vendor labels in
`docs/audit/v1427-summary.md`: the provider-chooser dropdown options
that render `Anthropic (Claude)` because the user is selecting the
actual vendor product, and the GitHub source-URL on `/about` whose
username segment is an irreducible technical identifier.
2026-05-15 20:57:47 +02:00
Marc-André Bombeck 3b045d6fba chore(insights): rename the advisor title key to neutral phrasing across six locales
Replace `insights.aiAnalysisTitle` with `insights.advisorTitle` across
all six locale bundles and the four call sites in
`insight-advisor-card.tsx`. EN renders "Personal health advisor", DE
"Persönlicher Berater" matching the card subtitle vocabulary, and
FR/ES/IT/PL pick up native phrasings instead of the English fallback
that previously leaked verbatim on those locales. The card now reads
in the same voice as the rest of the Coach and advisor surfaces.
2026-05-15 20:54:54 +02:00
Marc-André Bombeck 1ca7022538 chore(i18n): fan filterByType across the remaining locale files
`measurements.filterByType` shipped in en + de with the
v1.4.27 measurement-list aria-label wiring; the locale-integrity
test failed because the four English-fallback locales (es, fr, it,
pl) had not been backfilled. Add the placeholder English string so
each locale file is key-parity with `messages/en.json` again; real
translations follow in the v1.4.28 locale sweep.
2026-05-15 18:44:10 +02:00
Marc-André Bombeck bba8921ec9 chore(measurements,medications): apply the tap-target floor across list and form surfaces
Lift the icon-button and pagination controls on the list + form
surfaces under `/measurements` and `/medications` so every touch
target clears the 44 px WCAG 2.5.5 floor.

- `measurement-list.tsx`: mobile-row edit + delete buttons `size-8`
  → `size-11`; pagination chevrons `size="sm"` → `size="icon"
  size-11` with translated `aria-label`; edit-dialog kebab
  `h-9 w-9` → `size-11`; `DeleteButton` default `h-8 w-8` →
  `size-11`. Empty-state CTAs drop `size="sm"` so they sit at the
  default 40 px primitive height.
- `measurement-form.tsx`: reset kebab `h-9 w-9` → `size-11`.
- `intake-history-list.tsx`: desktop + mobile per-row edit + delete
  buttons `size-8` → `size-11`; pagination chevrons → `size-11`
  icon-buttons; edit-dialog kebab → `size-11`; shared `DeleteButton`
  in this file → `size-11`.
- `SideEffectsSection.tsx`: per-entry delete `h-7 w-7` → `size-11`.
- `inventory-section.tsx`: live-pen "mark as in use" / "mark as used
  up" pills drop `size="sm"`/`h-7` so they sit at `min-h-11`; past-pen
  delete `h-6 w-6` → `size-11` icon-button.

Adds `measurements.previousPage` / `measurements.nextPage` and
`medications.previousPage` / `medications.nextPage` translation keys
so the pagination icon-buttons carry accessible labels in every
locale.
2026-05-15 18:41:52 +02:00
Marc-André Bombeck 22b11427e8 feat(i18n): add notification, insights empty-state, and admin carrier keys plus drift-guard
Lands every new translation key referenced by the v1.4.27 buckets B1,
B3, B4 and B5 across all six locale bundles (38 keys per locale):

- dashboard.glp1.tabLevel / tabWeight / tabsAria / rangeStripLabel /
  levelUnavailable / weightUnavailable (B1 — drug-level vs weight tab
  strip on the GLP-1 dashboard tile).
- admin.carrier / admin.carrierUnknown (B3 — IP-address carrier chip
  on the admin login overview).
- insights.emptyState.{bloodPressure,weight,pulse,bmi,mood,medication,sleep}.{title,description,cta}
  (B4 — per-metric empty-state cards on the insights sub-pages).
- notifications.admin.{deployFailedTitle,deployFailedBody,testNotificationTitle,testNotificationBody,reminderCheckMissedTitle,reminderCheckMissedBody,reminderCheckOverdueTitle,reminderCheckOverdueBody}
  (B5 — admin notification messages routed through the localised
  dispatch helper).
- notifications.user.telegramTestBody (B5 — user-facing Telegram test
  message localised to User.locale).

Also retires the now-dead charts.moodLabel1..5 chart-axis label keys:
the mood chart and the mood list now consume the same MOOD_LABEL_KEYS
map from a new src/lib/mood/labels.ts so the chart-axis copy stays in
lockstep with the list (BL-P6-11). Three distinct copy sets for the
same five mood states drifted twice during v1.4.18 / v1.4.25
polishing; the shared resolver locks them together going forward.

New drift-guard test at src/__tests__/i18n-drift-guard.test.ts pins
the GLP-1 tab, admin carrier, insights empty-state, admin/user
notification, and personal-record key sets so a contributor who only
edits one bundle gets a precise failure instead of a 200-line parity
diff. The locale-integrity test continues to assert global parity;
the drift-guard layers on top with call-site-specific coverage.
2026-05-15 17:22:35 +02:00
Marc-André Bombeck 19b5129306 feat(i18n): land locale-native date format ordering for FR/ES/IT/PL
Adds a new `format` namespace across every locale bundle that
documents the locale's native date / time pattern ordering as
template strings (`{day}.{month}.{year}`, `{month}/{day}/{year}`, …).
DE keeps DD.MM.YYYY with dot separators, EN keeps MM/DD/YYYY with
slashes, FR/ES/IT adopt DD/MM/YYYY with slashes (the previous
EN-shaped fallback was visibly wrong on French weekly-report PDFs),
PL adopts DD.MM.YYYY with dots.

The runtime `makeFormatters()` helper still routes through `Intl.*`;
the keys exist so PDF / CSV / email surfaces that render outside a
React context can still pick the right ordering hint.

A new smoke test at
`src/lib/i18n/__tests__/format-locale-order.test.ts` pins the
ordering per locale so a copy-paste regression can't silently flip
DD/MM to MM/DD on a non-English bundle.
2026-05-15 17:12:54 +02:00
Marc-André Bombeck 2960f7352c chore(i18n): retire 414 dead translation keys across six locale bundles
Sweeps 154 dead keys per locale bundle (~924 strings across six
locales) flagged by the v1.4.27 dead-key audit. Drops the
insights.*Status* trio (loading / noKey / unavailable) across six
metrics, the legacy `insights.aiInsights` / `dashboard.insightsPreview`
roots that B1 replaced with the gated tab strip, the
`insights.healthScore.askCoach` / `coachPrompt` pair retired with the
inline button, the orphan onboarding.v2.* stub set, plus the
ad-hoc `medications.glp1.drug.*.class` and `notifications.eventPersonalRecord*`
strings that lost their callers in v1.4.25/W17.

Also drops the stale prefill-format test under
`health-score-card.test.tsx` that asserted the now-removed coachPrompt
key shape — the negative-assertion test for the retired inline button
stays in place.

The locale-integrity test already validates the new shape: no
re-baselining needed because the test asserts parity + no-empties
rather than a key-count threshold.
2026-05-15 17:11:26 +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.
2026-05-15 07:35:05 +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.
2026-05-14 23:49:32 +02:00