Merge branch 'fix/coldstart-perf' into release/v1.27.32

This commit is contained in:
Marc-André Bombeck committed 2026-07-09 02:08:46 +02:00
commit 57c7972755
27 files changed
+1040 -179

No files matched your search

@@ -96,6 +96,19 @@ export function GeneralSettingsSection() {
disabled={updateSettings.isPending}
/>
{/* Honest nudge: registration stays open after the first admin so a
household can be invited, but on an internet-exposed instance that
means anyone who can reach the URL can sign up. Surface a quiet
reminder while it is open — no behaviour change, just a signal. */}
{(settings?.registrationEnabled ?? true) && (
<p
data-slot="registration-open-hint"
className="text-muted-foreground -mt-2 text-xs"
>
{t("admin.registrationOpenHint")}
</p>
)}
{/* v1.23 — instance-wide "require a second factor" policy. Every
account without an active factor is sent to forced enrollment after
sign-in. This is a web-session nudge, not an API wall: direct API /
@@ -15,6 +15,7 @@ import {
import { useTranslations } from "@/lib/i18n/context";
import { useWorkouts, type WorkoutListEntry } from "@/hooks/use-workouts";
import { cn } from "@/lib/utils";
import { getDateTimeFormat } from "@/lib/intl/formatter-cache";
/**
* v1.4.32 — `<RecentWorkoutsTile>`.
@@ -70,7 +71,7 @@ function formatDuration(seconds: number): string {
function formatDayLabel(iso: string, locale: string): string {
const d = new Date(iso);
return new Intl.DateTimeFormat(locale, {
return getDateTimeFormat(locale, {
weekday: "short",
day: "2-digit",
month: "short",
+5 -4
View File
@@ -17,6 +17,7 @@ import {
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { getNumberFormat, getDateTimeFormat } from "@/lib/intl/formatter-cache";
import { useTranslations, useTimeFormatPreference } from "@/lib/i18n/context";
import {
hourCycleOptions,
@@ -73,7 +74,7 @@ function formatDuration(seconds: number): string {
function formatDistanceKm(meters: number, locale: string): string {
const km = meters / 1000;
return new Intl.NumberFormat(locale, {
return getNumberFormat(locale, {
maximumFractionDigits: 2,
minimumFractionDigits: km < 10 ? 2 : 1,
}).format(km);
@@ -84,7 +85,7 @@ function formatNumber(
locale: string,
fractionDigits = 0,
): string {
return new Intl.NumberFormat(locale, {
return getNumberFormat(locale, {
maximumFractionDigits: fractionDigits,
minimumFractionDigits: fractionDigits,
}).format(value);
@@ -111,13 +112,13 @@ function formatDateRange(
): string {
const start = new Date(startedAt);
const end = new Date(endedAt);
const dateFmt = new Intl.DateTimeFormat(locale, {
const dateFmt = getDateTimeFormat(locale, {
weekday: "long",
day: "2-digit",
month: "long",
year: "numeric",
});
const timeFmt = new Intl.DateTimeFormat(locale, {
const timeFmt = getDateTimeFormat(locale, {
hour: "2-digit",
minute: "2-digit",
...hourCycleOptions(timeFormat),
+4 -3
View File
@@ -13,6 +13,7 @@ import {
} from "lucide-react";
import { cn } from "@/lib/utils";
import { getNumberFormat, getDateTimeFormat } from "@/lib/intl/formatter-cache";
import { useTranslations, useTimeFormatPreference } from "@/lib/i18n/context";
import {
hourCycleOptions,
@@ -68,14 +69,14 @@ function formatDuration(seconds: number): string {
function formatDistanceKm(meters: number, locale: string): string {
const km = meters / 1000;
return new Intl.NumberFormat(locale, {
return getNumberFormat(locale, {
maximumFractionDigits: 2,
minimumFractionDigits: km < 10 ? 2 : 1,
}).format(km);
}
function formatEnergy(kcal: number, locale: string): string {
return new Intl.NumberFormat(locale, {
return getNumberFormat(locale, {
maximumFractionDigits: 0,
}).format(Math.round(kcal));
}
@@ -86,7 +87,7 @@ function formatDate(
timeFormat: TimeFormatPreference,
): string {
const d = new Date(iso);
return new Intl.DateTimeFormat(locale, {
return getDateTimeFormat(locale, {
weekday: "short",
day: "2-digit",
month: "short",
@@ -10,6 +10,7 @@ import { MedicationCardBody } from "@/components/medications/card-parts/medicati
import { useWeekdayLabel } from "@/components/medications/card-parts/medication-next-last-slot";
import { formatTimeWindowRange } from "@/lib/time-window-format";
import { formatDateTime, formatTime } from "@/lib/format";
import { getDateTimeFormat } from "@/lib/intl/formatter-cache";
import { getMedicationCategoryLabel } from "@/lib/medications/category-label";
import { formatDose } from "@/lib/medications/format-dose";
import {
@@ -335,7 +336,7 @@ export function MedicationCard({
// Intentionally en-CA: gives YYYY-MM-DD which is locale-independent and
// string-comparable for the today / yesterday / older bucketing below.
// The actual user-facing display goes through formatTime / formatDateTime.
const dayFormatter = new Intl.DateTimeFormat("en-CA", {
const dayFormatter = getDateTimeFormat("en-CA", {
timeZone: userTz,
year: "numeric",
month: "2-digit",
+78 -45
View File
@@ -12,6 +12,7 @@ import {
useContext,
useEffect,
useState,
useSyncExternalStore,
type ReactNode,
} from "react";
import { I18nProvider, useTranslations } from "@/lib/i18n/context";
@@ -63,59 +64,91 @@ function applyTheme(resolved: "dark" | "light") {
document.documentElement.classList.add(resolved);
}
// ── Theme as an external store ───────────────────────
//
// The stored preference lives in localStorage, and the OS preference in
// matchMedia — both browser-only values that must NOT be read in a `useState`
// initializer (server renders "dark", a client with a stored "light"/"system"
// initializes differently → the React #418 hydration seam this repo already
// hit with the sidebar). `useSyncExternalStore` is the sanctioned fix: it reads
// the value with an SSR-stable server snapshot ("dark") and reconciles to the
// real value after hydration without a setState-in-effect. The nonce-bound
// inline script in layout.tsx stamps the correct class pre-paint, so there is
// no visual FOUC while React reconciles.
const THEME_STORAGE_KEY = "healthlog-theme";
const themeListeners = new Set<() => void>();
function notifyThemeListeners() {
for (const cb of themeListeners) cb();
}
function subscribeStoredTheme(onChange: () => void): () => void {
themeListeners.add(onChange);
const onStorage = (e: StorageEvent) => {
if (e.key === THEME_STORAGE_KEY) onChange();
};
window.addEventListener("storage", onStorage);
return () => {
themeListeners.delete(onChange);
window.removeEventListener("storage", onStorage);
};
}
function getStoredThemeSnapshot(): Theme {
const saved = localStorage.getItem(THEME_STORAGE_KEY) as Theme | null;
// A stored "light"/"dark"/"system" choice wins. With no stored preference the
// app defaults to dark rather than tracking the OS.
if (saved === "light" || saved === "dark" || saved === "system") return saved;
return "dark";
}
function getServerThemeSnapshot(): Theme {
return "dark";
}
function subscribeSystemTheme(onChange: () => void): () => void {
const mq = window.matchMedia("(prefers-color-scheme: dark)");
mq.addEventListener("change", onChange);
return () => mq.removeEventListener("change", onChange);
}
function getSystemThemeSnapshot(): "dark" | "light" {
return getSystemTheme();
}
function getServerSystemSnapshot(): "dark" | "light" {
return "dark";
}
function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => {
if (typeof window === "undefined") return "dark";
const saved = localStorage.getItem("healthlog-theme") as Theme | null;
// A stored "light"/"dark"/"system" choice wins. With no stored
// preference the app defaults to dark rather than tracking the OS.
if (saved === "light" || saved === "dark") return saved;
if (saved === "system") return "system";
return "dark";
});
const theme = useSyncExternalStore(
subscribeStoredTheme,
getStoredThemeSnapshot,
getServerThemeSnapshot,
);
const systemTheme = useSyncExternalStore(
subscribeSystemTheme,
getSystemThemeSnapshot,
getServerSystemSnapshot,
);
const resolvedTheme = theme === "system" ? systemTheme : theme;
const [resolvedTheme, setResolvedTheme] = useState<"dark" | "light">(() => {
if (typeof window === "undefined") return "dark";
const saved = localStorage.getItem("healthlog-theme") as Theme | null;
if (saved === "light" || saved === "dark") return saved;
if (saved === "system") return getSystemTheme();
return "dark";
});
// Apply theme on mount (inline script handles FOUC, this ensures classes are in sync)
// Keep the <html> class in sync with the resolved theme. This is a DOM write,
// not a setState — it runs on mount (reconciling the class the inline script
// set to any stored preference) and whenever the resolved theme changes.
useEffect(() => {
applyTheme(resolvedTheme);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Listen for OS preference changes when in system mode
useEffect(() => {
if (theme !== "system") return;
const mq = window.matchMedia("(prefers-color-scheme: dark)");
const handler = (e: MediaQueryListEvent) => {
const resolved = e.matches ? "dark" : "light";
setResolvedTheme(resolved);
applyTheme(resolved);
};
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}, [theme]);
}, [resolvedTheme]);
const setTheme = useCallback((next: Theme) => {
setThemeState(next);
// Persist every explicit choice, including "system" — the absence of a
// stored value is reserved for a fresh visitor and now defaults to dark.
// stored value is reserved for a fresh visitor and defaults to dark.
// Storing "system" verbatim keeps an explicit OS-tracking choice
// distinguishable from "never chose", so a reload honours it.
localStorage.setItem("healthlog-theme", next);
if (next === "system") {
const resolved = getSystemTheme();
setResolvedTheme(resolved);
applyTheme(resolved);
} else {
setResolvedTheme(next);
applyTheme(next);
}
// distinguishable from "never chose", so a reload honours it. The write +
// notify re-reads the external store; the effect above applies the class.
localStorage.setItem(THEME_STORAGE_KEY, next);
notifyThemeListeners();
}, []);
return (