diff --git a/CHANGELOG.md b/CHANGELOG.md index 033305bc0..e19126240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## [Unreleased] +## [1.25.4] — 2026-06-29 — Time picker honors the 24-hour preference + +A focused fix release. No schema change. + +### Fixed + +- **The time picker now follows your 24-hour preference.** The earlier fix corrected the times shown across the app, but tapping a date/time field still opened the browser's own time picker — which renders its clock (and the AM/PM toggle) by the browser's language, not the app's. On an English-language browser you saw AM/PM even with 24-hour selected. The field now uses its own time picker, so hours/minutes always follow your preference (24-hour, 12-hour, or automatic) in every browser. The date half keeps the familiar native calendar. +- The doctor-report PDF's "generated at" timestamp now honors the time-format preference as well, instead of falling back to the locale default. +- When adding a blood-pressure reading, the systolic / diastolic / pulse field labels no longer wrap onto a second line in the three-column layout — the metric name stays prominent and its unit (mmHg / bpm) sits beside it as a compact affix. + ## [1.25.3] — 2026-06-29 — Mental wellbeing, labs depth, and fixes A patch release: a clearer mental-wellbeing check-in, more useful lab pages, two visible bug fixes, and a couple of settings tidy-ups. No schema change. diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 737849280..1424b7c65 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.25.3 + version: 1.25.4 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/messages/de.json b/messages/de.json index a1bba3e2e..b636d0959 100644 --- a/messages/de.json +++ b/messages/de.json @@ -21,6 +21,12 @@ "disabled": "Deaktiviert", "networkError": "Netzwerkfehler", "openDatePicker": "Datumsauswahl öffnen", + "openTimePicker": "Uhrzeitauswahl öffnen", + "hours": "Stunden", + "minutes": "Minuten", + "period": "AM/PM", + "am": "AM", + "pm": "PM", "unknownError": "Unbekannter Fehler", "copied": "Kopiert!", "or": "oder", diff --git a/messages/en.json b/messages/en.json index d5e7f3509..eb170fd61 100644 --- a/messages/en.json +++ b/messages/en.json @@ -21,6 +21,12 @@ "disabled": "Disabled", "networkError": "Network error", "openDatePicker": "Open date picker", + "openTimePicker": "Open time picker", + "hours": "Hours", + "minutes": "Minutes", + "period": "AM/PM", + "am": "AM", + "pm": "PM", "unknownError": "Unknown error", "copied": "Copied!", "or": "or", diff --git a/messages/es.json b/messages/es.json index 2c5779350..e72662bbf 100644 --- a/messages/es.json +++ b/messages/es.json @@ -21,6 +21,12 @@ "disabled": "Desactivado", "networkError": "Error de red", "openDatePicker": "Abrir el selector de fecha", + "openTimePicker": "Abrir el selector de hora", + "hours": "Horas", + "minutes": "Minutos", + "period": "a. m./p. m.", + "am": "a. m.", + "pm": "p. m.", "unknownError": "Error desconocido", "copied": "¡Copiado!", "or": "o", diff --git a/messages/fr.json b/messages/fr.json index 66b5b8e3f..72c7b9298 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -21,6 +21,12 @@ "disabled": "Désactivé", "networkError": "Erreur réseau", "openDatePicker": "Ouvrir le sélecteur de date", + "openTimePicker": "Ouvrir le sélecteur d’heure", + "hours": "Heures", + "minutes": "Minutes", + "period": "AM/PM", + "am": "AM", + "pm": "PM", "unknownError": "Erreur inconnue", "copied": "Copié !", "or": "ou", diff --git a/messages/it.json b/messages/it.json index 14c32c857..1baba41ad 100644 --- a/messages/it.json +++ b/messages/it.json @@ -21,6 +21,12 @@ "disabled": "Disabilitato", "networkError": "Errore di rete", "openDatePicker": "Apri il selettore di data", + "openTimePicker": "Apri il selettore di orario", + "hours": "Ore", + "minutes": "Minuti", + "period": "AM/PM", + "am": "AM", + "pm": "PM", "unknownError": "Errore sconosciuto", "copied": "Copiato!", "or": "o", diff --git a/messages/pl.json b/messages/pl.json index ff87fd7d6..91255fac6 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -21,6 +21,12 @@ "disabled": "Wyłączone", "networkError": "Błąd sieci", "openDatePicker": "Otwórz wybór daty", + "openTimePicker": "Otwórz wybór godziny", + "hours": "Godziny", + "minutes": "Minuty", + "period": "AM/PM", + "am": "AM", + "pm": "PM", "unknownError": "Nieznany błąd", "copied": "Skopiowano!", "or": "lub", diff --git a/package.json b/package.json index 22e4dead2..7ec973958 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.25.3", + "version": "1.25.4", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "PolyForm-Noncommercial-1.0.0", "homepage": "https://healthlog.dev", diff --git a/public/sw.js b/public/sw.js index 8082ed455..ac9f0c378 100644 --- a/public/sw.js +++ b/public/sw.js @@ -36,7 +36,7 @@ try { // v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.25.0"; + /* @sw-version-fallback */ "v1.25.4"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*` diff --git a/src/__tests__/i18n-english-leak-guard.test.ts b/src/__tests__/i18n-english-leak-guard.test.ts index f71f44fa2..743a46c16 100644 --- a/src/__tests__/i18n-english-leak-guard.test.ts +++ b/src/__tests__/i18n-english-leak-guard.test.ts @@ -75,6 +75,9 @@ function isMaskPlaceholder(words: string[]): boolean { * reason. Prune an entry only when the value genuinely diverges in a locale. */ const LEGIT_IDENTICAL = new Set([ + // The AM/PM marker label — the affix abbreviations themselves are the same + // 12-hour-clock notation across the shipped Latin-script locales. + "common.period", // Clinical abbreviations / SI units — identical across locales. "charts.vo2Max", "cycle.insights.crosstab.unitGlucose", diff --git a/src/app/api/export/health-record/route.ts b/src/app/api/export/health-record/route.ts index 5c81f9372..83253d1ff 100644 --- a/src/app/api/export/health-record/route.ts +++ b/src/app/api/export/health-record/route.ts @@ -119,7 +119,11 @@ export const POST = apiHandler(async (request: NextRequest) => { resolveUserTimezone(user.id), prisma.user.findUnique({ where: { id: user.id }, - select: { insuranceNumberEncrypted: true, insightsCachedText: true }, + select: { + insuranceNumberEncrypted: true, + insightsCachedText: true, + timeFormat: true, + }, }), // v1.25 (W-RECORDS) — structured records are always-available reference // data (not time-windowed), so they ride alongside the report rather @@ -203,6 +207,7 @@ export const POST = apiHandler(async (request: NextRequest) => { t, locale, userTz, + timeFormat: userRow?.timeFormat ?? "AUTO", insuranceNumber, includeCharts, aiSummary, diff --git a/src/components/measurements/measurement-form.tsx b/src/components/measurements/measurement-form.tsx index 85733c329..40b296d82 100644 --- a/src/components/measurements/measurement-form.tsx +++ b/src/components/measurements/measurement-form.tsx @@ -190,6 +190,30 @@ function getDefaultMeasuredAtValue() { return local.toISOString().slice(0, 16); } +/** + * Field label that keeps the "Name (unit)" translation on a single line even + * in the narrow three-column blood-pressure grid: the metric name truncates if + * it must, while the unit renders as a small muted affix that never wraps. The + * label strings stay localised — we split the trailing "(unit)" the catalogue + * already carries rather than dropping it (so "mmHg" / "bpm" / locale variants + * like "lpm" survive). + */ +function UnitLabel({ htmlFor, label }: { htmlFor: string; label: string }) { + const match = /^(.*?)\s*\(([^)]*)\)\s*$/.exec(label); + const name = match ? match[1] : label; + const unit = match ? match[2] : null; + return ( + + ); +} + export function MeasurementForm({ onSuccess, onCancel, @@ -383,7 +407,7 @@ export function MeasurementForm({ {isBpMode ? (
- +
- +
- + {t("medications.detail.zeitplan.window.startLabel")} - - setRow(row.timeOfDay, { start: e.target.value }) - } - className="h-11 w-32 sm:h-9" + onChange={(v) => setRow(row.timeOfDay, { start: v })} + className="h-11 w-32 sm:h-9 sm:min-h-9" aria-label={t( "medications.detail.zeitplan.window.startLabel", )} - data-slot="dose-window-start" + data-testid="dose-window-start" /> – - - setRow(row.timeOfDay, { end: e.target.value }) - } - className="h-11 w-32 sm:h-9" + onChange={(v) => setRow(row.timeOfDay, { end: v })} + className="h-11 w-32 sm:h-9 sm:min-h-9" aria-label={t( "medications.detail.zeitplan.window.endLabel", )} - data-slot="dose-window-end" + data-testid="dose-window-end" />
{isValidHhmm(row.start) && diff --git a/src/components/medications/scheduling/times-of-day-chips.tsx b/src/components/medications/scheduling/times-of-day-chips.tsx index 3ae3b7f22..ae306bcc6 100644 --- a/src/components/medications/scheduling/times-of-day-chips.tsx +++ b/src/components/medications/scheduling/times-of-day-chips.tsx @@ -33,7 +33,7 @@ import { useCallback, useId, useMemo, useState } from "react"; import { Plus, X } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; +import { TimeField } from "@/components/ui/time-field"; import { Label } from "@/components/ui/label"; import { useTranslations } from "@/lib/i18n/context"; @@ -145,13 +145,11 @@ export function TimesOfDayChips({ - { - const v = e.target.value; + onChange={(v) => { if (!v) { onChange([]); return; @@ -159,7 +157,7 @@ export function TimesOfDayChips({ if (!TIME_RE.test(v)) return; onChange([v]); }} - className="h-11 w-32 sm:h-9" + className="h-11 w-32 sm:h-9 sm:min-h-9" aria-label={t(`${i18nPrefix}.label`)} />
@@ -253,15 +251,14 @@ export function TimesOfDayChips({ - setDraft(e.target.value)} - className="h-11 w-32 sm:h-9" + onChange={(v) => setDraft(v)} + className="h-11 w-32 sm:h-9 sm:min-h-9" aria-label={t(`${i18nPrefix}.add`)} - data-slot="times-of-day-draft-input" + data-testid="times-of-day-draft-input" /> + className="w-32 shrink-0" + aria-label={ariaLabel} + aria-required={ariaRequired} + /> ); }); -/** - * Format a local `yyyy-MM-ddTHH:mm` string into "