From 2d630994a2c415f766e8bce81d1082b1270b3f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 16 May 2026 17:11:27 +0200 Subject: [PATCH] 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`). --- messages/de.json | 3 +- messages/en.json | 3 +- messages/es.json | 3 +- messages/fr.json | 3 +- messages/it.json | 3 +- messages/pl.json | 3 +- src/components/settings/timezone-picker.tsx | 20 +++++++++-- src/components/ui/date-input.tsx | 40 ++++++++++++++++++--- 8 files changed, 66 insertions(+), 12 deletions(-) diff --git a/messages/de.json b/messages/de.json index 57f95616c..c8d46db99 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1639,7 +1639,8 @@ "genderHint": "Wird für geschlechtsspezifische Zielwerte verwendet.", "timezone": "Zeitzone", "timezoneHint": "Wird für Diagrammachsen, Erinnerungszeiten, Export-Zeitstempel und den Coach-Kontext verwendet. Gespeicherte Daten bleiben unverändert.", - "timezoneDetect": "Browser-Zeitzone übernehmen ({tz})", + "timezoneDetect": "Browser-Zeitzone übernehmen", + "timezoneDetectAria": "Browser-Zeitzone {tz} übernehmen", "timezoneInvalid": "Keine gültige IANA-Zeitzone.", "profileSaved": "Profil gespeichert", "changePassword": "Passwort ändern", diff --git a/messages/en.json b/messages/en.json index effd8cddd..26db86590 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1639,7 +1639,8 @@ "genderHint": "Used for gender-specific target values.", "timezone": "Timezone", "timezoneHint": "Used for chart axis labels, reminder times, export timestamps, and Coach context. Stored data is unchanged.", - "timezoneDetect": "Use browser timezone ({tz})", + "timezoneDetect": "Use browser timezone", + "timezoneDetectAria": "Use browser timezone {tz}", "timezoneInvalid": "Not a valid IANA timezone.", "profileSaved": "Profile saved", "changePassword": "Change password", diff --git a/messages/es.json b/messages/es.json index b9a2f3b83..8774fdf7e 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1639,7 +1639,8 @@ "genderHint": "Used for gender-specific target values.", "timezone": "Zona horaria", "timezoneHint": "Used for chart axis labels, reminder times, export timestamps, and Coach context. Stored data is unchanged.", - "timezoneDetect": "Use browser timezone ({tz})", + "timezoneDetect": "Use browser timezone", + "timezoneDetectAria": "Use browser timezone {tz}", "timezoneInvalid": "Not a valid IANA timezone.", "profileSaved": "Profile saved", "changePassword": "Change password", diff --git a/messages/fr.json b/messages/fr.json index 9ad6defc2..60481947c 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1639,7 +1639,8 @@ "genderHint": "Used for gender-specific target values.", "timezone": "Fuseau horaire", "timezoneHint": "Used for chart axis labels, reminder times, export timestamps, and Coach context. Stored data is unchanged.", - "timezoneDetect": "Use browser timezone ({tz})", + "timezoneDetect": "Use browser timezone", + "timezoneDetectAria": "Use browser timezone {tz}", "timezoneInvalid": "Not a valid IANA timezone.", "profileSaved": "Profile saved", "changePassword": "Change password", diff --git a/messages/it.json b/messages/it.json index b4c5c9f82..08b458dc5 100644 --- a/messages/it.json +++ b/messages/it.json @@ -1639,7 +1639,8 @@ "genderHint": "Used for gender-specific target values.", "timezone": "Fuso orario", "timezoneHint": "Used for chart axis labels, reminder times, export timestamps, and Coach context. Stored data is unchanged.", - "timezoneDetect": "Use browser timezone ({tz})", + "timezoneDetect": "Use browser timezone", + "timezoneDetectAria": "Use browser timezone {tz}", "timezoneInvalid": "Not a valid IANA timezone.", "profileSaved": "Profile saved", "changePassword": "Change password", diff --git a/messages/pl.json b/messages/pl.json index 7cfd5ac43..3eecc932e 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -1639,7 +1639,8 @@ "genderHint": "Used for gender-specific target values.", "timezone": "Strefa czasowa", "timezoneHint": "Used for chart axis labels, reminder times, export timestamps, and Coach context. Stored data is unchanged.", - "timezoneDetect": "Use browser timezone ({tz})", + "timezoneDetect": "Use browser timezone", + "timezoneDetectAria": "Use browser timezone {tz}", "timezoneInvalid": "Not a valid IANA timezone.", "profileSaved": "Profile saved", "changePassword": "Change password", diff --git a/src/components/settings/timezone-picker.tsx b/src/components/settings/timezone-picker.tsx index f26157d9c..f336a5f30 100644 --- a/src/components/settings/timezone-picker.tsx +++ b/src/components/settings/timezone-picker.tsx @@ -80,9 +80,14 @@ export function TimezonePicker({ variant="outline" size="sm" onClick={handleUseBrowserTz} + aria-label={ + detected + ? t("settings.timezoneDetectAria", { tz: detected }) + : undefined + } > - {t("settings.timezoneDetect", { tz: detected })} + {t("settings.timezoneDetect")} ); @@ -109,15 +114,26 @@ export function TimezonePicker({ ))} + {/* v1.4.33 — visible label is "use browser zone"; the actual + IANA zone is read out via aria-label so screen-reader users + still hear which zone the click will apply. Previously the + label inlined `(Europe/Berlin)`, which on a 393 CSS px + viewport wrapped the button to two lines and broke height + parity with the adjacent select. */}

{labelHint}

diff --git a/src/components/ui/date-input.tsx b/src/components/ui/date-input.tsx index 91c72adfb..395c83e3b 100644 --- a/src/components/ui/date-input.tsx +++ b/src/components/ui/date-input.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; import { useTranslations } from "@/lib/i18n/context"; /** @@ -13,16 +14,47 @@ import { useTranslations } from "@/lib/i18n/context"; * having to pull `locale` out of `useTranslations()` just to feed it * back into the same JSX prop, and guarantees a future date input * cannot silently regress by forgetting the hint. + * + * v1.4.33 — height parity with `` and ``. + * + * Mobile Safari + Android Chrome give `type="date"` (and + * `type="datetime-local"`) an intrinsic chrome that puffs the + * field past Tailwind's `h-10`, so the DOB cell on + * `/settings/account` rendered ~6 px taller than its row siblings + * and roughly 12 px wider on the Pixel-5 mobile-pass screenshots. + * `appearance-none` switches the native widget off, then + * `min-h-10` re-asserts the contract that every other input + * primitive ships. The `:webkit-date-and-time-value` shadow rule + * forces the embedded value text to occupy at least one full line + * even when iOS shrinks its placeholder, so the field looks + * identical on both engines. */ +const DATE_INPUT_HEIGHT_CLASSES = + "appearance-none min-h-10 h-10 [&::-webkit-date-and-time-value]:min-h-[1.5em]"; + type InputProps = React.ComponentProps; type DateInputProps = Omit; -export function DateInput(props: DateInputProps) { +export function DateInput({ className, ...props }: DateInputProps) { const { locale } = useTranslations(); - return ; + return ( + + ); } -export function DateTimeInput(props: DateInputProps) { +export function DateTimeInput({ className, ...props }: DateInputProps) { const { locale } = useTranslations(); - return ; + return ( + + ); }