Merge pull request #720 from MBombeck/fix/handover-p1-batch

Four data fixes: backfilled doses, impossible readings, stale today-claims, sleep units
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-08-07 19:32:40 +02:00
commit 3778e70ff5
19 files changed
+1408 -30

No files matched your search

@@ -37,6 +37,7 @@ function deviation(
high: 62,
outside,
direction: outside ? "above" : "in",
daysAgo: 0,
};
}
+16 -6
View File
@@ -3,8 +3,9 @@
import { useQuery } from "@tanstack/react-query";
import { Activity, ArrowDownRight, ArrowUpRight } from "lucide-react";
import { useTranslations } from "@/lib/i18n/context";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import { useAuth } from "@/hooks/use-auth";
import { metricFractionDigits } from "@/lib/measurements/value-domain";
import { cn } from "@/lib/utils";
import { apiGet } from "@/lib/api/api-fetch";
import { queryKeys } from "@/lib/query-keys";
@@ -44,6 +45,7 @@ export function HealthStatusCard({
className,
}: HealthStatusCardProps) {
const { t } = useTranslations();
const fmt = useFormatters();
const { isAuthenticated } = useAuth();
const { data } = useQuery({
@@ -60,6 +62,14 @@ export function HealthStatusCard({
return key ? t(key) : type;
};
// The engines hand over raw doubles. Interpolating one straight into the
// sentence prints whatever floating-point produced — a pulse mean reads
// "68.33333333333333" — and reads in the wrong number convention for every
// locale that does not use a dot. Render each figure at the metric's own
// decimal precision, through the same formatter the rest of the app uses.
const metricValue = (type: string, value: number): string =>
fmt.number(value, metricFractionDigits(type));
return (
<section
data-slot="health-status-section"
@@ -100,9 +110,9 @@ export function HealthStatusCard({
</p>
<p className="text-muted-foreground text-xs leading-snug">
{t("insights.healthStatus.rangeContext", {
value: d.value,
low: d.low,
high: d.high,
value: metricValue(d.type, d.value),
low: metricValue(d.type, d.low),
high: metricValue(d.type, d.high),
})}
</p>
</div>
@@ -137,8 +147,8 @@ export function HealthStatusCard({
</p>
<p className="text-muted-foreground text-xs leading-snug">
{t("insights.healthStatus.shiftContext", {
before: s.beforeMean,
after: s.afterMean,
before: metricValue(s.metric, s.beforeMean),
after: metricValue(s.metric, s.afterMean),
})}
</p>
</div>
@@ -26,6 +26,10 @@ import { Loader2, MoreHorizontal, Plus, RotateCcw } from "lucide-react";
import { toast } from "sonner";
import { useTranslations } from "@/lib/i18n/context";
import { useUnitDisplay } from "@/hooks/use-unit-display";
import {
entryValueToCanonical,
parseDecimalEntry,
} from "@/lib/measurements/entry-units";
import {
invalidateKeys,
measurementDependentKeys,
@@ -343,7 +347,17 @@ export function MeasurementForm({
// identity path — `fromDisplay` returns the typed number unchanged, so
// the payload is byte-identical to before. Round only the converted
// imperial value so 210 lb stores as 95.25 kg, not 95.25439770….
const typed = parseFloat(value);
//
// A second boundary sits beside it: a few types are stored in a unit
// the form does not ask for. Sleep is entered in hours and stored in
// minutes, and sending the typed number through unconverted filed a
// 7.5-hour night as seven and a half minutes — inside the column's
// plausibility band, so nothing objected.
const typed = parseDecimalEntry(value);
if (typed === null) {
setError(t("measurements.saveError"));
return;
}
let canonicalValue = typed;
if (unitDisplay.isTransformed(type)) {
const inverted = unitDisplay.fromDisplay(type, typed);
@@ -352,6 +366,7 @@ export function MeasurementForm({
? Math.round(inverted * 100) / 100
: inverted;
}
canonicalValue = entryValueToCanonical(type, canonicalValue);
await apiPost("/api/measurements", {
type,
value: canonicalValue,
@@ -533,9 +548,13 @@ export function MeasurementForm({
>
<Input
id="value"
type="number"
// Not `type="number"`: its sanitisation drops a decimal comma on
// the engines that do not localise the field, and "7,5" then
// arrives as an empty string with no explanation. `inputMode`
// still raises the numeric keypad, and the submit path reads the
// comma — the same shape the labs and custom-metric fields use.
inputMode="decimal"
enterKeyHint="next"
step="any"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={