Commit Graph
166 Commits
Author SHA1 Message Date
Marc-André BombeckandClaude Opus 4.7 ae637ce844 i18n(charts): translate chart legends, thresholds, and comparison copy into es/fr/it/pl
Covers the data-visualisation surface: chart loading/empty states,
sample windows (7/30/90 pts), weekday and month abbreviations,
heatmap and adherence legends, personal-baseline deltas, chart-overlay
controls, target thresholds (metric labels, override warning, save
toasts), and the period-over-period comparison toggle. Placeholders
{delta}, {min}, {max}, {unit} preserved across all locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:19:55 +02:00
Marc-André BombeckandClaude Opus 4.7 1e9bde7b60 i18n(shell): translate auth, nav, password-strength, and trend-hints into es/fr/it/pl
Localises the user-facing shell strings: auth (sign-in / sign-up,
passkey fallback, password validation), nav (sidebar labels, theme
toggle), password-strength meter, trend-hints countdown, notifications
breadcrumbs, common copy/unknownError, format.dateShort with locale
date order (dd/mm/yyyy for es/fr/it, dd.mm.yyyy for pl), and the
errorBoundary fallback title. Placeholders {minLength}, {count} all
preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:17:33 +02:00
Marc-André BombeckandClaude Opus 4.7 507295ecca i18n(mood): translate mood scale, tags, and empty-state copy into es/fr/it/pl
Localises the 25 mood-namespace EN-fallback keys: 5-point scale labels
(super gut/okay/lausig surfaced as Excellent/Bien/Pénible, etc.),
tags field, filters, empty states, validation errors, and the
delete-confirm copy. Keeps the German `levelSuperGut`/`levelOkay`/
`levelLausig` key identifiers intact while translating the rendered
text per locale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:16:21 +02:00
Marc-André BombeckandClaude Opus 4.7 3407753a77 i18n(measurements): translate medical labels and form copy into es/fr/it/pl
Covers metric type names (weight, blood pressure, glucose, body
composition, Apple Health audio exposure, walking metrics), glucose
context options, source labels, form field labels, validation errors,
empty states, and pagination controls. 62 keys per locale, preserving
{count}/{unit}/{page}/{total} placeholders and the mmHg/bpm conventions
common to each medical tradition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:15:40 +02:00
Marc-André BombeckandClaude Opus 4.7 94d90f31ea i18n(dashboard): translate greeting, layout, and empty-state copy into es/fr/it/pl
Localises the remaining 27 keys per locale across dashboard.greeting,
welcomeBack, streakLabel, vo2Max, bpInTarget, quickAdd shortcuts,
compliance7d, the layout-customisation cluster, and the empty-state
hero. Keeps placeholders ({greeting}, {name}, {count}) intact and
matches the calm professional register used in the German source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:14:15 +02:00
Marc-André BombeckandClaude Opus 4.7 5c737f9ac8 i18n(medication-intake): translate W7b quick-add cluster into es/fr/it/pl
Replaces 14 English-fallback strings in dashboard.medicationIntakeQuickAdd
and dashboard.quickAddMedicationIntake (plus the W7c step-consolidation
captions in measurements.dailyTotalCaption/expandDay/collapseDay) with
native Spanish, French, Italian, and Polish copy. Closes the v1.4.37
W10 i18n-findings gap on the four secondary locales for the medication
intake and measurement-grouping surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:13:26 +02:00
Marc-André Bombeck c72b3ce890 fix(geo-backfill): drop batch cap from 5000 to 500 rows per pass
Each row spends up to 3 s on the lookupIpLocation online fallback;
5000 rows × 3 s let a single backfill burst block for ~4 h and starve
the next hourly cron. Tighten the cap to 500 so the worst-case pass
fits well inside the hour-long schedule slot. The helper is
idempotent, so a tenant with a large null-location backlog drains
over multiple hourly passes instead of one mega-pass.
2026-05-17 17:12:33 +02:00
Marc-André Bombeck bd841aee99 fix(dashboard): clarify dose field is informational only in medication quick-add
The dose field on the medication-intake quick-add sheet was rendered
editable but the POST body has no `dose` slot — any user edit was
silently dropped, so a user typing "5 mg" for a half-pill day saw the
catalogue dose recorded instead. The previous hint copy did not warn
about the constraint, leaving the user's mental model out of sync
with the persisted data.

Render the field read-only with a muted background, drop the tab
order on the input, and rewrite the hint copy across all six locales
so the user knows that variable doses are coming in a later version
and the standard dose is recorded for now. The setDoseOverride state
holder stays so the reset-on-medication-switch keeps working when the
v1.4.38 doseOverride schema lands.
2026-05-17 15:29:56 +02:00
Marc-André Bombeck c42a62d03f feat(i18n): medication-intake quick-add labels across six locales
Add the menu-item label (dashboard.quickAddMedicationIntake) and the
dashboard.medicationIntakeQuickAdd block (sheet title, sheet
description, field labels, hint, error message, and empty-state copy)
to all six locales. The German bundle ships Marc-Voice copy with full
UTF-8 umlauts; the other locales mirror the English fallback already
in use for the other two quick-add rows until the localisation pass.
2026-05-17 14:42:40 +02:00
Marc-André Bombeck 675c084baa ui(medications): move GLP-1 side-effect button into the card header actions overflow
The GLP-1 card carried a third Stethoscope button in the primary
actions row (Eingenommen / Übersprungen / Nebenwirkung), which broke
the two-button symmetry the generic medication card establishes.
Marc reported the asymmetry during the v1.4.37 UX audit (audit
item 11): "ich möchte natürlich, dass das genau gleich ist".

Move the side-effect quick-log into a kebab-style overflow in the
header `actions` slot. The kebab only mounts when the parent wires
`onLogSideEffect`, so Mounjaro and Ramipril cards stay byte-equivalent
in the common case (history + edit, no kebab). When the side-effect
hand-off is wired, Mounjaro gains the kebab while the primary actions
row stays the canonical two-button shape.

The localised label re-uses the existing `common.moreOptions` key for
the trigger aria-label and the existing `medications.glp1LogSideEffect`
key for the menu item, so no new i18n strings are required.
2026-05-17 14:32:40 +02:00
Marc-André Bombeck d2d0cbdc3b feat(export): i18n keys for Arztbericht hero across six locales
Adds a new `settings.sections.export.hero.{eyebrow,valueStatement,cta,
formatHint}` bag plus a `settings.sections.export.otherOptionsHeading`
key across DE, EN, FR, ES, IT, PL so the doctor-report hero card and
the "Other export options" sub-heading resolve everywhere. Retires the
now-unreferenced `settings.sections.export.cards.doctorReport.action`
key — the hero owns the CTA copy from this point on.

DE + EN ship fully translated; the four AI-initial locales (FR, ES, IT,
PL) receive native translations for the hero copy in line with the rest
of their export bag.
2026-05-17 14:24:13 +02:00
Marc-André Bombeck 7200fc8015 chore(i18n): drop orphaned update-card keys from settings.about
The dedicated update card was removed in 0738ef50 but its seven
copy keys (updatesHeading, checkUpdates, checking, lastChecked,
lastCheckedNever, upToDate, checkFailed) stayed in all six
locales. Only newerAvailable is still referenced — by the
UpdateBadge. Sweep the 42 unused strings.
2026-05-17 13:02:40 +02:00
Marc-André Bombeck 0ec7fc6fae fix(about): use distinct builtAtLabel key for the build-time row
The build-time row read its label from `t("builtAt", { time: "" }).trim()`
with a fallback to the gitSha key. The fallback was effectively dead
(the trim never produced an empty string) and the pattern silently
risked rendering two side-by-side "Build" columns when an
implementer dropped the trailing `{time}` placeholder. Replace with
a dedicated `settings.about.builtAtLabel` key across all six locales.
2026-05-17 13:02:07 +02:00
Marc-André Bombeck fa0f544e3f fix(audit): fall back to city/country when ASN lookup misses
The carrier chip on the admin login-overview audit table was empty
whenever the offline GeoLite2-ASN MMDB had no entry for the IP. In
practice this is every IP behind a non-DACH or cloud-NAT range — the
free-tier MMDB carries DACH carriers densely but thins out
elsewhere — and the chip went blank even though the city + country
were already populated from the parallel location lookup.

When `entry.carrier` is null but `entry.location` is non-null,
render the chip with the city/country string and a "carrier
unavailable" qualifier. Private/loopback IPs still hide the chip
entirely (both columns null).
2026-05-17 12:41:34 +02:00
Marc-André Bombeck 23cf9a4a78 ux(about): fold About card into the Admin Console
The About surface (version + license + source links + update check)
used to live as its own Settings section and the user-card dropdown
exposed it from both desktop and mobile. The three cards collectively
get opened once or twice a year, mostly by the operator confirming
the running version against the latest release.

Move the surface to `/admin/about` and drop the dropdown link from
both the desktop sidebar and the mobile top-bar. The public-facing
landing at `/about` (credits + MaxMind attribution) stays untouched
since it's reachable without a session. The legacy `/settings/about`
route still resolves so external bookmarks don't break, but is no
longer surfaced in any nav.

The existing `<AboutSection>` is reused as-is — no markup changes,
the component owns its own heading so the admin renderer mounts it
without the SectionFrame wrapper.
2026-05-17 12:35:50 +02:00
Marc-André Bombeck 1e9a03bee5 feat(coach): medications + anthropometrics exclusion toggles
Extend the coach exclude-metrics enum with two optional-context tokens
and surface them in the settings sheet under a separate "Include
context" group. The snapshot gates the GLP-1 weeklyContext block on
the medications toggle (skipping the Prisma lookup entirely when
excluded) and gates the new anthropometrics block on the
anthropometrics toggle plus at least one non-null profile field.
Empty blocks are never emitted.
2026-05-17 12:25:30 +02:00
Marc-André Bombeck a17e7a46af feat(medications): restore intake-history table on the detail page
The v1 IntakeHistoryList was retired in v1.4.28 on the assumption
it had no other consumer. The standalone /medications/[id]/history
page lost its dose timeline as a side-effect — non-GLP-1 medications
now render only the heading. This restores a slim, read-only v2.

- Paginated 25/page via the existing limit/offset query params
- Sortable by takenAt (default desc) and scheduledFor
- Status and source badges; no inline edit or delete
- Empty state with CTA back to the daily intake page
- i18n keys across all six locales (DE + EN curated, the AI-initial
  locales mirror the EN copy per the v1.4.25 W9e maintainership policy)
2026-05-17 12:21:00 +02:00
Marc-André Bombeck 28a829695d fix(insights): tri-state render contract for trend annotations
The trends-row annotation slot painted "Mehr Daten nötig, um diesen
Trend zu kommentieren" whenever the advisor query was in flight or a
regenerate was firing — even when the model was about to deliver
fresh prose. Two cold-mount conditions collapsed into the same empty
hint: advisor pending, and advisor returned null.

`<TrendAnnotation>` now reads a tri-state `status` prop:

  - `pending`     — advisor query in flight / regenerate firing.
                    Renders a 3-line shimmer block.
  - `needs_data`  — advisor returned `annotation = null`. Renders the
                    "Mehr Daten nötig" hint.
  - `generated`   — advisor returned a string. Renders the prose +
                    optional confidence chip.

When `status` is omitted the legacy `annotation == null → empty`
mapping still applies (back-compat for existing callers).

`<TrendsRow>` derives the status from a new `loading` prop and the
per-metric annotation presence, with pending winning over needs_data
so a mid-regenerate cycle never flashes the empty hint between
shimmer and new prose. The Insights mother page now passes
`loading={advisor.isLoading || advisor.isRegenerating}`.

Added unit tests pin the three render-state branches plus the
back-compat fallback. New `trendAnnotation.pendingLabel` i18n key
across DE / EN / FR / ES / IT / PL drives the aria-label on the
pending block. Health Score paint lag is a parent-level analytics
query issue and stays for v1.4.37.
2026-05-17 12:19:43 +02:00
Marc-André Bombeck 1768b1d971 ux: i18n leaks, focus rings, tap-target floor, admin chip-scroll, empty-state CTA vocabulary
Eight hotfix-ready UX/a11y findings from the v1.4.34.3 UX audit
land as one batch.

i18n leaks (3 primitives):
  - Dialog + Sheet close-X carried hardcoded `<span sr-only>Close</span>`
    so a German user heard 'Close, button' on every modal. Both lift
    `useTranslations()` and read `common.close`.
  - ChartSkeleton's sr-only loading announcement was English-only.
    The component is already a client island; reads `charts.loadingLabel`
    from each locale now. New key shipped in DE / EN / FR / ES / IT / PL.
  - Notifications-section breadcrumb `<nav aria-label="Breadcrumb">`
    was hardcoded. Routes through `t("nav.breadcrumb")` with a new
    key in each locale.

Focus rings (mobile + tablet nav):
  - Bottom-nav primary links + the More overflow trigger relied on a
    `text-muted-foreground` → `text-primary` colour transition only;
    keyboard / switch-control users saw no focus indicator at all.
    Added `focus-visible:ring-ring/50 focus-visible:ring-2
    focus-visible:outline-none focus-visible:ring-offset-2` matching
    the v1.4.33 sidebar treatment.
  - Mobile top-bar trigger had `focus:outline-none` without a
    `focus-visible` replacement. Same treatment applied.

Tap-target floor (WCAG 2.5.5):
  - Input / NativeSelect / Select / DateTimeInput were locked to
    h-10 (40 px) on every viewport. Switched to `h-11 sm:h-10` so
    mobile clears the 44 px floor while desktop keeps the 40 px norm
    that the Button primitive already converged on in v1.4.33.

Admin chip-strip auto-scroll-into-view:
  - The admin shell shipped 13 chips but lacked the
    `scrollIntoView({inline: 'center'})` effect the settings shell
    got in v1.4.33. Admins tapping a chip past the right edge landed
    on the new page with the strip scrolled to the left and the
    active chip off-screen. Copied the settings-shell pattern verbatim.

List-page subtitles on mobile:
  - Measurements / Mood / Medications pages hid their descriptive
    subtitle on `<sm` with `hidden sm:block`. Comparable apps keep
    the subtitle on mobile because it doubles as contextual help.
    Dropped the hide rule; the H1 still anchors the visual.

Insights empty-state CTA vocabulary:
  - `ALLOWED_ADD_TYPES` on `/measurements` accepted tokens (`GLUCOSE`,
    `TEMPERATURE`, `HEART_RATE`, `BMI`) that didn't match the
    `MEASUREMENT_TYPES` enum the form uses. Several `/insights/*`
    empty-state CTAs emitted those mismatched tokens and the dialog
    silently failed to open. Replaced with a single source of truth
    derived from `MEASUREMENT_TYPES` plus a normalisation map for
    legacy tokens. Added BODY_TEMPERATURE to the form's enum row.
    New unit test walks every `?add=` token emitted under
    `src/app/insights/**` and asserts it resolves to a real form type.
2026-05-17 00:34:00 +02:00
Marc-André Bombeck 435021857a i18n: add Vitals parent + Targets & Sources keys across six locales
Two key additions land in every locale (de, en, es, fr, it, pl):

- `insights.tabStrip.vitalsParent.{label,header}` — the parent pill
  text and popover header for the collapsed wave-A vital pills.
- `settings.sections.thresholds.{title,description}` — renamed to
  "Targets & Sources" / "Zielwerte & Quellen" / "Objetivos y fuentes"
  / "Objectifs et sources" / "Obiettivi e fonti" / "Cele i źródła"
  (and matching descriptions).
- `settings.sections.sources.redirectNotice` — copy reserved for the
  back-compat redirect surface; reads "Sources are now part of
  'Targets & Sources'" in each locale.

The existing `settings.sections.sources.*` block stays in the
locale files because `<SourcesSection>` (now embedded inside the
Targets & Sources page) still resolves card-level labels through
those keys.

Per `.planning/research/v1434-r-2-carryover-scope.md` §7 + §8.
2026-05-16 20:48:21 +02:00
Marc-André Bombeck 721e88dfa8 feat(i18n): add stale-hint week/month plurals and dashboard coach CTA
The dashboard tile-strip `<TrendCard staleDays>` caption renders bucket-aware
copy so a tile whose latest reading is older than a week reads naturally:

* `staleDays > 7 && <= 30` — existing `dashboard.staleHint` ("Letzter Wert
  vor Xd")
* `30 < staleDays <= 60` — collapses to a weekly form ("vor X Wochen")
* `staleDays > 60` — collapses to a monthly form ("vor X Monaten")

Two new singular/plural key pairs per bucket — `staleHintWeeksOne` /
`staleHintWeeksOther`, `staleHintMonthsOne` / `staleHintMonthsOther` — so
locales with non-English plural rules read naturally. Polish takes the
genitive-plural fallback ("tygodni" / "miesięcy") which covers every
non-1 count the bucket math can produce.

Also adds `dashboard.coachCta` ("Frag den Coach" / "Ask the coach") for the
dashboard hero strip's Coach launch button — the global drawer is hoisted to
the auth-shell in a follow-up so every authed surface can call `askCoach()`
from the same context.

All six locales (de, en, es, fr, it, pl) updated.
2026-05-16 20:42:57 +02:00
Marc-André Bombeck bafac84f8d chore(i18n): retire the now-orphan settings.kiInsights key
The dedicated card H2 was retired in the previous "drop AI prefix"
pass; the `settings.kiInsights` translation key it consumed has no
remaining call-site in source. Drop it from de / en / es / fr / it /
pl. Also update the message-thread test to the new "Insights
provider" copy.
2026-05-16 18:02:59 +02:00
Marc-André Bombeck 0de1e2ebe7 fix(settings): fold the About section into the user-card dropdown
Eleven sections on the settings sidebar was one over the comfort
line. About is three small cards (identity, links, update check) that
collectively get read once or twice a year and didn't earn a top-level
slot. Hide it from the in-shell nav and surface it as "About HealthLog"
in the sidebar user-card dropdown (mobile dropdown mirrors). The
`/settings/about` route stays alive so direct links, the public
`/about` permalink, and the dropdown entry continue to resolve.
2026-05-16 17:59:52 +02:00
Marc-André Bombeck 972c8a56dd fix(nav): drop the redundant "Home" group label from the desktop sidebar
The sidebar has exactly one nav group, and the first entry is already
"Dashboard" pointing to `/`, so the "HOME / Dashboard" pairing used to
read as if there were two separate destinations for the homepage. Drop
the group label and retire the `nav.home` locale key in every bundle.
The collapse toggle keeps its position at the top-right of the strip.
2026-05-16 17:57:21 +02:00
Marc-André Bombeck 432ebccdae fix(i18n): drop the "KI"/"AI" prefix from user-facing copy
Marc-Voice rule — the product never names itself "KI" or "AI" in
copy. Settings -> "KI-Auswertungen" becomes "Auswertungen" / "Insights",
the inner "KI-Insights" card heading collapses into the parent section
heading, "Aktiver KI-Provider" becomes "Aktiver Provider", and the
admin "AI Quality" view becomes "Insights Quality".

Sweep across de / en / es / fr / it / pl. Provider product names
(Anthropic, Claude, ChatGPT, OpenAI) are unchanged — those are
proper nouns, not the "AI" label.
2026-05-16 17:56:34 +02:00
Marc-André Bombeck 523ee0c7d1 fix(notifications): disambiguate inbox vs channel-config naming
Both the `/notifications` inbox and the Settings -> Notifications
section used to read as plain "Notifications" in every nav surface, so
the user-card dropdown and the settings sidebar looked like two pointers
at the same destination. Rename the inbox to "Notification Center" and
the settings section to "Notification channels"; add a small breadcrumb
to either surface so the user always knows which page they landed on
and can jump to the sibling in one tap.

Locale parity across de / en / es / fr / it / pl.
2026-05-16 17:49:31 +02:00
Marc-André BombeckandClaude Opus 4.7 73043d42d1 fix(settings): tile-padding parity, passkey breakpoint, scroll-into-view, F17 threshold toggle
Five small polish items from the v1.4.33 audit, plus the
maintainer's scroll-into-view item:

* Export-section card padding promoted from `p-5` to `p-6` so the
  Export grid rhythm matches every other Settings card.
* Passkey list flips from a desktop table to a mobile card list at
  `lg` instead of `md`. iPad portrait (768 px) used to sit on the
  `md` inflection and flip-flop between the two layouts on
  rotation; the card layout reads fine on iPad portrait, the table
  needs ~620 px of column width to show the destructive delete
  action without horizontal scroll.
* `<SettingsShell>` mobile chip strip now scrolls its active chip
  into view on route change (`block: "nearest", inline: "center",
  behavior: "smooth"`). On a 393 CSS px viewport the 11-chip strip
  pushes the rightmost four chips below the fold; without the
  scroll-into-view, tapping one of those chips left the strip
  pinned to the leftmost chip on the new page, which read as a
  broken nav. Smooth scroll honours the user's
  `prefers-reduced-motion` via the browser's own auto-detection.
* F17 — `thresholds-editor-section` toggle label anchors on the
  *action* it performs ("Eigene Werte" / "Custom range") instead
  of mirroring the current state ("Auto" / "Überschrieben"). The
  old label read as "Auto is off, why are there no inputs?" when
  the toggle was off and inputs were hidden; the new label
  unambiguously announces "flip me to enter a custom range". The
  `thresholds.sourceOverride` badge still surfaces when the
  override is the active source. Adds
  `thresholds.overrideToggleLabel`.

Locale parity across de, en, es, fr, it, pl.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:35:07 +02:00
Marc-André Bombeck 99af530490 chore(cleanup): retire AssistantDisabledNotice + dead settings.placeholder copy
Dead-code sweep flagged by the v1.4.33 menu audit.

* AssistantDisabledNotice (src/components/insights/assistant-disabled-notice.tsx)
  was introduced in v1.4.31 for the operator-disabled assistant surface
  but never wired into a render site. The v1.4.28 retirement of the
  weekly-report + advisor card removed the planned host components.
  Delete the component, its SSR test, and the four locale strings it
  consumed (insights.briefingDisabledByOperator,
  insights.statusDisabledByOperator, insights.correlationsDisabledByOperator,
  insights.coach.disabledByOperator) from all six locale files. The
  AssistantDisabledError thrown by requireAssistantSurface() and the
  assistant.disabled.<surface> errorCode envelope stay — iOS clients
  read the errorCode to render an inline notice.

* settings.sections.placeholder.coming_soon was an "until extraction
  lands" string for <SectionPlaceholder>. Every slug in
  SETTINGS_SECTION_SLUGS has had a matching component since v1.4.22
  so the placeholder body is unreachable through the type system. The
  defensive component itself stays as a guard, but inherits the
  section's own description for the EmptyState body, retiring the
  dedicated locale key from all six locale files.

Locale-integrity contract holds (parity check green across de, en, es,
fr, it, pl). Comments in api-handler.ts and feature-flags/index.ts
that referenced <AssistantDisabledNotice> are updated to describe
the surface generically.

Refs: .planning/round-v1433-audit-menu.md §6
2026-05-16 17:31:47 +02:00
Marc-André BombeckandClaude Opus 4.7 d4a1679ec6 i18n(settings): parity for the trimmed Notifications description
The notifications.description copy compression that landed in
ff06b0ce moved the four non-de/en bundles into drift. Apply the
same one-sentence description ("Live overview of every configured
channel.") across es, fr, it, pl so the parity test stays green
and the four still-translated locales don't keep the old 13-word
sentence as a fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:30:37 +02:00
Marc-André BombeckandClaude Opus 4.7 ff06b0ced2 fix(settings): collapse Notifications redundancy onto one status surface
Telegram, ntfy, and Web Push cards used to paint their own
"Configured · Disabled / Active" badge pair in the top-right
header. The `<NotificationStatusCard>` rendered above the same
cards already shows a live channel pill ("Active · 12 min ago" /
"Auto-disabled — web_push_410_gone"), so the per-card badges were
the third (sometimes fourth) surface for the same state.

Drop the dual-Badge slot from all three cards. The section header
pill is the single source of truth, the per-card surface now
reads as identity + description + form, and the in-app vocabulary
no longer mixes "Konfiguriert", "Aktiv", and "Verbunden" for the
same fact.

Copy pass:
* Settings → Notifications description: "Live-Übersicht aller
  konfigurierten Kanäle." (was a 13-word sentence repeating "Kanal"
  three times).
* `notificationStatus.title` renames to "Zustellstatus" /
  "Delivery status" so the page header and the first card don't
  both anchor on "Kanal".
* `notificationStatus.description` compresses the "Auto-disabled"
  definition.
* Telegram card title drops "-Benachrichtigungen" — the section is
  Notifications, so saying it again is wasted shelf space.
* `telegramDescription` shortens to "Medikamenten-Erinnerungen
  per Bot." / "Medication reminders via bot."
* `moodLogDescription` drops the second "moodLog" mention.

Notification-status SSR test updated to the new copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:29:37 +02:00
Marc-André BombeckandClaude Opus 4.7 c98d07effd fix(settings): F13 username readability, F14 mobile bottom-nav padding, Mood Log overflow
Three closely-related polish fixes from the v1.4.33 audit:

* `/settings/account` username field flips `disabled` to `readOnly`.
  The disabled state inherits `opacity-50` from the `<Input>` primitive,
  which on Marc's mobile pass read identically to placeholder text
  and made the field look empty. `readOnly` keeps the full-contrast
  value rendering while still preventing edits.
* `<SettingsShell>` main column reserves a 96 px bottom gutter on
  `<md` so the last form field on `/settings/account` (Gender) and
  `/settings/ai` (Active provider) is not eaten by the floating
  mobile bottom-nav. Desktop keeps the parent's own padding because
  the bottom-nav doesn't render on `md+`.
* Mood Log webhook-secret row stacks vertically on `<sm` with
  `break-all` on the input value, killing the body-level horizontal
  scroll triggered by the long hex secret on Pixel 5. The
  Sync / Voll-Sync / Trennen triplet picks up the Withings
  `flex-wrap` + `min-w-[10rem]` pattern, and the in-form save row
  right-aligns its primary action so it matches every other
  integration form.
* New locale key `settings.moodLogFullSyncConfirm`
  ("Vollständig synchronisieren" / "Synchronize fully") is now
  wired into the Voll-Sync confirmation dialog so the trigger and
  confirm actions read as distinct copy.

Locale parity across de, en, es, fr, it, pl.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:15:26 +02:00
Marc-André Bombeck 7b78a4bf39 fix(insights): gate /api/insights/generate POST on assistant feature flag
Every visit to /insights or any sub-route used to fire POST
/api/insights/generate from InsightsLayoutShell. The mutation
returned null on 422 (no provider configured) so the UI degraded
gracefully, but the server kept absorbing the call on every cold
mount and the dev-server logs filled with '422 Unprocessable
Entity'. Read the operator's assistant flag matrix from
/api/feature-flags (already cached server-side at 60s, client-side
under the shared ['feature-flags'] query key) and skip the mutation
entirely when 'enabled' or 'briefing' is false. The fail-open
default in useFeatureFlags means a flags-endpoint hiccup still
renders the advisor — the gate only takes effect when the operator
has explicitly turned the surface off.

Also update the per-metric InsightStatusCard's empty-state copy
from the diagnostic 'KI-Provider nicht konfiguriert' / 'AI
provider not configured' to the neutral 'Auswertung nicht
verfügbar' / 'Assessment unavailable'. The diagnostic phrasing
read as a setup failure even though the surface degrades cleanly;
the neutral phrasing matches the rest of the empty-state language.

Per .planning/round-v1433-audit-runtime.md F18.
2026-05-16 17:15:10 +02:00
Marc-André Bombeck 743579ca8f fix(insights): /insights/puls subtitle reads 'Puls' not 'Ruhepuls'
The /insights/puls page reads spot-pulse measurements (PULSE) while
/insights/ruhepuls covers the daily resting-heart-rate stream
(RESTING_HEART_RATE). Both routes shared a single copy thread that
labelled /insights/puls as 'Ruhepuls', so the strip pill said one
thing and the description said another. The Karvonen target band
applies to live pulse just as well as to resting HR, so the wording
swap is the only change — no semantics shift.

Per .planning/round-v1433-audit-runtime.md F7.
2026-05-16 17:12:49 +02:00
Marc-André Bombeck 2d630994a2 refactor(insights): fold Math.min/max spreads in the six status helpers
`bmi`, `pulse`, `general`, `weight`, `blood-pressure`, and `mood`
status helpers all built their `summarizeSeries` block with
`Math.min(...series.map(...))` and `Math.max(...series.map(...))`.
The same anti-pattern hit `summarize()` in `src/lib/analytics/trends.ts`
on 2026-05-16 and crashed `/api/analytics` with
`RangeError: Maximum call stack size exceeded` once an
Apple-Health-synced PULSE series outgrew V8's ~125 000-arg ceiling;
`61107e0c` folded that one. The six insight helpers are fed bounded
windows today so they never reached the ceiling, but the spread still
allocates a transient args array on every call and the fold is both
stack-safe and cheaper.

Replace each `Math.min(...) / Math.max(...) / average(...)` triplet
with a single walk that maintains running sum / min / max. The
now-unused `average()` helper is dropped from each file. Pinned by
the existing per-helper unit tests (326 passing in
`src/lib/insights` / `src/lib/analytics` / `src/app/api/analytics`).
2026-05-16 17:11:27 +02:00
Marc-André Bombeck 3da0b38e0f fix(i18n): drop placeholder cta keys on the wave-A empty-state blocks
The four metric sub-pages without a manual-entry CTA
(`hrv`, `restingHr`, `oxygenSaturation`, `activeEnergy`) shipped
with `"cta": ""` placeholders that broke the locale-integrity
sentinel. The template never renders those keys when
`emptyStateCtaType` is null — drop the empty values so every
remaining key carries non-empty text.
2026-05-16 15:54:42 +02:00
Marc-André Bombeck 1f01b941ae feat(insights): chart cards for HRV, resting HR, SpO2, body temperature, active energy
Adds five sub-pages for the wave-A HealthKit metrics the server has
been storing since v1.4.23 / v1.4.25 but never surfaced on the web:

  - /insights/hrv               → HEART_RATE_VARIABILITY
  - /insights/ruhepuls          → RESTING_HEART_RATE
  - /insights/sauerstoff        → OXYGEN_SATURATION
  - /insights/koerpertemperatur → BODY_TEMPERATURE
  - /insights/aktive-energie    → ACTIVE_ENERGY_BURNED

Each page composes the shared `<HealthKitMetricPage>` scaffold —
`SubPageShell` + `HealthChartDynamic` mounted on its own
`ChartOverlayKey` slot + `CoachLaunchButton` + an empty-state CTA
that points at the existing `/measurements?add=<TYPE>` quick-entry
where a manual entry makes sense, or an Apple-Health-onboarding cue
otherwise. The five new chart-overlay-prefs slots (`hrv`,
`restingHr`, `oxygenSaturation`, `bodyTemperature`, `activeEnergy`)
let the chart-cog popover persist per metric.

Insights tab strip picks up the five new pills via the existing
`SUB_PAGE_METRIC` + `InsightMetric` enums. The wave-A categorisation
overlay realigns HRV + resting HR to the `cardiovascular` bucket
per the iOS handoff brief (PULSE stays in `vitals`; spot-pulse is a
point-in-time signal, derived heart-rate variants are the cardiac
risk surface). Six locales updated with the new sub-page titles,
descriptions, chart titles, and empty-state copy.
2026-05-16 15:53:18 +02:00
Marc-André Bombeck 3f5816112b feat(dashboard): recent workouts tile
Adds `<RecentWorkoutsTile>` as a default-visible dashboard widget
(`recentWorkouts`). The tile reads the top three canonical workouts
through `useWorkouts({ limit: 3 })`, renders each as a deep-link
row with the sport icon + the day + the duration, and falls back
to an Apple-Health-onboarding cue when the workout list is empty.

`DASHBOARD_WIDGET_IDS` + `DEFAULT_DASHBOARD_LAYOUT.widgets` carry
the new id; `widgetIdEnum` on `/api/dashboard/widgets` picks it up
from the same source-of-truth array. The Settings → Dashboard
layout editor surfaces it under the existing widget toggle list so
users can opt out without touching the layout JSON. Six locales
updated.
2026-05-16 15:47:44 +02:00
Marc-André Bombeck beaee5791a feat(insights): workouts list page at /insights/workouts
Adds the user-visible surface for the workout rows the iOS client
already syncs through `POST /api/workouts/batch`. The page lives at
`/insights/workouts`, consumes the v1.4.32 list endpoint through a
new `useWorkouts()` hook, and renders each row as a deep-link into
the detail page with the sport icon, the date, the duration, and
the optional distance + active-energy chips.

The insights tab strip gains a `workouts` pill driven by an
event-style `hasWorkouts` availability signal — the layout shell
probes the list endpoint with `limit=1` so the pill self-gates on
zero workouts. Empty state mounts the shared `MetricEmptyState`
primitive with a Coach prefill that steers the conversation toward
the Apple Health sync onboarding cue. Six locales updated.
2026-05-16 15:38:07 +02:00
Marc-André Bombeck e778835c9c i18n: localise the operator-disabled assistant notices
Four user-facing keys land in all six locale files so the
`<AssistantDisabledNotice>` component reads natively localised
copy:

  insights.coach.disabledByOperator
  insights.briefingDisabledByOperator
  insights.statusDisabledByOperator
  insights.correlationsDisabledByOperator

The Coach key nests under the existing `insights.coach.*` object;
the other three sit at the top of `insights.*` to avoid colliding
with the existing string-valued `insights.correlations` key.

The Health-Score explainer has no notice — its suppression is
silent on purpose so the bare delta digit still reads as a clean
KPI.

DE carries the full native phrasing; the established convention
for the other five locales (FR/ES/IT/PL/EN) keeps the user-facing
strings localised even where admin strings still default to
English.
2026-05-16 14:02:22 +02:00
Marc-André Bombeck 3b56bac59a feat(admin): assistant feature-flag toggle panel
New admin slug `assistant` between `ai-quality` (provider config)
and `coach-feedback` (in-app feedback inbox). The panel renders six
`<SettingsToggle>` rows — master plus five sub-flags — and writes
through a dedicated `PUT /api/admin/settings/assistant-flags`
endpoint. Optimistic UI flips on click; failure surfaces a toast
and re-invalidates the read query.

The dedicated endpoint (separate from the generic `/api/admin/settings`
PUT) lets the panel scope its audit trail to a single
`admin.settings.assistant-flags.update` action and lets the
optimistic UI bind 1:1 to a focused request/response shape. The
PUT handler echoes both the raw column values and the resolved
shape so the panel can render the master vs sub-flag distinction
visually.

Sub-toggles grey out when the master is off (UX-wise) so the
operator can see which sub-flags are individually flipped before
unmuting the master. Writes invalidate the runtime
`/api/feature-flags` cache so the operator sees the toggled
surface react within the same session.

i18n: `admin.section.assistant.{title,subtitle}` and
`admin.assistant.*` keys land in all six locales (DE proper, the
five English-leaning admin locales mirror the English copy as is
the established convention for admin strings).
2026-05-16 14:00:10 +02:00
Marc-André Bombeck 0522652ae2 feat(measurements): wire walking-steadiness + audio-exposure-event through every registry
v1.4.30 commit 7 follow-up. The new MeasurementType values land in
every derived registry the canonical enum drives:

  - measurement-list-meta.ts label keys + icons + colors so the
    measurements list view renders both types (Gauge for steadiness,
    Volume2 for the loud-listening event)
  - 6 locale files (de/en/es/fr/it/pl) carry typeWalkingSteadiness +
    typeAudioExposureEvent keys; English-fallback copy for the four
    non-EN/DE locales matches the existing v1.4.25 W8d pattern
  - chart-tokens.ts ALLOWED_CHART_TOKENS list so the LLM can
    reference `metric:WALKING_STEADINESS` + `metric:AUDIO_EXPOSURE_EVENT`
    in prose; dedicated chart components ship alongside the iOS sync
  - the enum-coverage drift-guard test's EXPECTED_TYPES + PDF
    exclusion set so the canonical enum-extension lint stays green

Full unit suite passes (4035 specs).
2026-05-16 12:23:30 +02:00
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