mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Merge pull request #277 from MBombeck/release/v1.15.2
v1.15.2 — cycle insights crash fix
This commit is contained in:
11 files changed
+132
-5
No files matched your search
@@ -2,6 +2,12 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.15.2] — 2026-06-06 — cycle insights crash fix
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Cycle insights no longer crash on mood or blood-glucose relations.** The insights tab could throw and fail to render when a mood or blood-glucose pattern cleared the significance check, because those two metrics were missing their on-screen labels and units. They now render correctly, and an unrecognised metric can never crash the tab again.
|
||||
|
||||
## [1.15.1] — 2026-06-06 — cycle premium parity, reminder polish, fixes
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: HealthLog API
|
||||
version: 1.15.1
|
||||
version: 1.15.2
|
||||
description: >-
|
||||
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
|
||||
|
||||
|
||||
@@ -4958,12 +4958,16 @@
|
||||
"metricBasalBodyTemp": "Basaltemperatur",
|
||||
"metricWristTemperature": "Handgelenkstemperatur nachts",
|
||||
"metricSkinTemperature": "Hauttemperatur",
|
||||
"metricBloodGlucose": "Blutzucker",
|
||||
"metricMood": "Stimmung",
|
||||
"unitHours": "Std.",
|
||||
"unitSteps": "Schritte",
|
||||
"unitBpm": "S/min",
|
||||
"unitMs": "ms",
|
||||
"unitKg": "kg",
|
||||
"unitCelsius": "°C",
|
||||
"unitGlucose": "mg/dL",
|
||||
"unitMood": "Pkt.",
|
||||
"confidenceLow": "Geringe Sicherheit",
|
||||
"confidenceMedium": "Mittlere Sicherheit",
|
||||
"confidenceHigh": "Hohe Sicherheit"
|
||||
|
||||
@@ -4958,12 +4958,16 @@
|
||||
"metricBasalBodyTemp": "Basal body temperature",
|
||||
"metricWristTemperature": "Overnight wrist temperature",
|
||||
"metricSkinTemperature": "Skin temperature",
|
||||
"metricBloodGlucose": "Blood glucose",
|
||||
"metricMood": "Mood",
|
||||
"unitHours": "h",
|
||||
"unitSteps": "steps",
|
||||
"unitBpm": "bpm",
|
||||
"unitMs": "ms",
|
||||
"unitKg": "kg",
|
||||
"unitCelsius": "°C",
|
||||
"unitGlucose": "mg/dL",
|
||||
"unitMood": "pts",
|
||||
"confidenceLow": "Low confidence",
|
||||
"confidenceMedium": "Medium confidence",
|
||||
"confidenceHigh": "High confidence"
|
||||
|
||||
@@ -4958,12 +4958,16 @@
|
||||
"metricBasalBodyTemp": "Temperatura basal",
|
||||
"metricWristTemperature": "Temperatura de la muñeca nocturna",
|
||||
"metricSkinTemperature": "Temperatura de la piel",
|
||||
"metricBloodGlucose": "Glucosa en sangre",
|
||||
"metricMood": "Estado de ánimo",
|
||||
"unitHours": "h",
|
||||
"unitSteps": "pasos",
|
||||
"unitBpm": "lpm",
|
||||
"unitMs": "ms",
|
||||
"unitKg": "kg",
|
||||
"unitCelsius": "°C",
|
||||
"unitGlucose": "mg/dL",
|
||||
"unitMood": "pts",
|
||||
"confidenceLow": "Confianza baja",
|
||||
"confidenceMedium": "Confianza media",
|
||||
"confidenceHigh": "Confianza alta"
|
||||
|
||||
@@ -4958,12 +4958,16 @@
|
||||
"metricBasalBodyTemp": "Température basale",
|
||||
"metricWristTemperature": "Température du poignet la nuit",
|
||||
"metricSkinTemperature": "Température de la peau",
|
||||
"metricBloodGlucose": "Glycémie",
|
||||
"metricMood": "Humeur",
|
||||
"unitHours": "h",
|
||||
"unitSteps": "pas",
|
||||
"unitBpm": "bpm",
|
||||
"unitMs": "ms",
|
||||
"unitKg": "kg",
|
||||
"unitCelsius": "°C",
|
||||
"unitGlucose": "mg/dL",
|
||||
"unitMood": "pts",
|
||||
"confidenceLow": "Confiance faible",
|
||||
"confidenceMedium": "Confiance moyenne",
|
||||
"confidenceHigh": "Confiance élevée"
|
||||
|
||||
@@ -4958,12 +4958,16 @@
|
||||
"metricBasalBodyTemp": "Temperatura basale",
|
||||
"metricWristTemperature": "Temperatura del polso notturna",
|
||||
"metricSkinTemperature": "Temperatura della pelle",
|
||||
"metricBloodGlucose": "Glicemia",
|
||||
"metricMood": "Umore",
|
||||
"unitHours": "h",
|
||||
"unitSteps": "passi",
|
||||
"unitBpm": "bpm",
|
||||
"unitMs": "ms",
|
||||
"unitKg": "kg",
|
||||
"unitCelsius": "°C",
|
||||
"unitGlucose": "mg/dL",
|
||||
"unitMood": "pts",
|
||||
"confidenceLow": "Affidabilità bassa",
|
||||
"confidenceMedium": "Affidabilità media",
|
||||
"confidenceHigh": "Affidabilità alta"
|
||||
|
||||
@@ -4958,12 +4958,16 @@
|
||||
"metricBasalBodyTemp": "Temperatura podstawowa",
|
||||
"metricWristTemperature": "Nocna temperatura nadgarstka",
|
||||
"metricSkinTemperature": "Temperatura skóry",
|
||||
"metricBloodGlucose": "Glukoza we krwi",
|
||||
"metricMood": "Nastrój",
|
||||
"unitHours": "godz.",
|
||||
"unitSteps": "kroki",
|
||||
"unitBpm": "ud./min",
|
||||
"unitMs": "ms",
|
||||
"unitKg": "kg",
|
||||
"unitCelsius": "°C",
|
||||
"unitGlucose": "mg/dL",
|
||||
"unitMood": "pkt",
|
||||
"confidenceLow": "Niska pewność",
|
||||
"confidenceMedium": "Średnia pewność",
|
||||
"confidenceHigh": "Wysoka pewność"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "healthlog",
|
||||
"version": "1.15.1",
|
||||
"version": "1.15.2",
|
||||
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
|
||||
"license": "AGPL-3.0-only",
|
||||
"homepage": "https://healthlog.dev",
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import {
|
||||
CyclePhaseCrosstab,
|
||||
CyclePhaseHeadline,
|
||||
type CyclePhaseCrosstabRow,
|
||||
type CyclePhaseCrosstabDisplay,
|
||||
} from "../cycle-phase-crosstab";
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
function render(node: React.ReactNode) {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">{node}</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
function row(
|
||||
metricKey: string,
|
||||
display: CyclePhaseCrosstabDisplay,
|
||||
): CyclePhaseCrosstabRow {
|
||||
return {
|
||||
metricKey,
|
||||
display,
|
||||
lutealDays: 12,
|
||||
follicularDays: 11,
|
||||
lutealAvg: 5,
|
||||
follicularAvg: 4,
|
||||
delta: 1,
|
||||
pValue: 0.01,
|
||||
qValue: 0.02,
|
||||
confidence: "high",
|
||||
};
|
||||
}
|
||||
|
||||
describe("<CyclePhaseCrosstab>", () => {
|
||||
// Regression: the server emits `display: "mood"` and `display: "glucose"`,
|
||||
// which the client UNIT_KEY map did not cover. The map miss handed `undefined`
|
||||
// to `t()`, whose resolver runs `key.split(".")` → "Cannot read properties of
|
||||
// undefined (reading 'split')", crashing the whole insights tab.
|
||||
it("renders mood and glucose rows without throwing", () => {
|
||||
expect(() =>
|
||||
render(
|
||||
<CyclePhaseCrosstab
|
||||
rows={[row("mood", "mood"), row("bloodGlucose", "glucose")]}
|
||||
/>,
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("resolves the mood + glucose metric labels and units", () => {
|
||||
const html = render(
|
||||
<CyclePhaseCrosstab
|
||||
rows={[row("mood", "mood"), row("bloodGlucose", "glucose")]}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain("Mood");
|
||||
expect(html).toContain("Blood glucose");
|
||||
expect(html).toContain("pts");
|
||||
expect(html).toContain("mg/dL");
|
||||
// Never leaks the raw metric key or the raw i18n key on a covered row.
|
||||
expect(html).not.toContain("cycle.insights.crosstab");
|
||||
});
|
||||
|
||||
it("renders the headline for a mood row without throwing", () => {
|
||||
expect(() =>
|
||||
render(<CyclePhaseHeadline headline={row("mood", "mood")} />),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("degrades an unknown display to an empty unit instead of crashing", () => {
|
||||
const unknown = row(
|
||||
"somethingNew",
|
||||
"unmappedDisplay" as unknown as CyclePhaseCrosstabDisplay,
|
||||
);
|
||||
expect(() => render(<CyclePhaseCrosstab rows={[unknown]} />)).not.toThrow();
|
||||
expect(() =>
|
||||
render(<CyclePhaseHeadline headline={unknown} />),
|
||||
).not.toThrow();
|
||||
// The delta still renders; the unit segment is simply blank (trailing space
|
||||
// trimmed away in markup), never the literal "undefined".
|
||||
const html = render(<CyclePhaseCrosstab rows={[unknown]} />);
|
||||
expect(html).not.toContain("undefined");
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,9 @@ export type CyclePhaseCrosstabDisplay =
|
||||
| "bpm"
|
||||
| "ms"
|
||||
| "kg"
|
||||
| "celsius";
|
||||
| "celsius"
|
||||
| "glucose"
|
||||
| "mood";
|
||||
|
||||
export type CyclePhaseCrosstabConfidence = "low" | "medium" | "high";
|
||||
|
||||
@@ -50,6 +52,8 @@ const METRIC_LABEL_KEY: Record<string, string> = {
|
||||
basalBodyTemp: "cycle.insights.crosstab.metricBasalBodyTemp",
|
||||
wristTemperature: "cycle.insights.crosstab.metricWristTemperature",
|
||||
skinTemperature: "cycle.insights.crosstab.metricSkinTemperature",
|
||||
bloodGlucose: "cycle.insights.crosstab.metricBloodGlucose",
|
||||
mood: "cycle.insights.crosstab.metricMood",
|
||||
};
|
||||
|
||||
const UNIT_KEY: Record<CyclePhaseCrosstabDisplay, string> = {
|
||||
@@ -59,6 +63,8 @@ const UNIT_KEY: Record<CyclePhaseCrosstabDisplay, string> = {
|
||||
ms: "cycle.insights.crosstab.unitMs",
|
||||
kg: "cycle.insights.crosstab.unitKg",
|
||||
celsius: "cycle.insights.crosstab.unitCelsius",
|
||||
glucose: "cycle.insights.crosstab.unitGlucose",
|
||||
mood: "cycle.insights.crosstab.unitMood",
|
||||
};
|
||||
|
||||
const CONFIDENCE_KEY: Record<CyclePhaseCrosstabConfidence, string> = {
|
||||
@@ -107,7 +113,10 @@ export function CyclePhaseHeadline({
|
||||
const metricLabel = t(
|
||||
METRIC_LABEL_KEY[headline.metricKey] ?? headline.metricKey,
|
||||
);
|
||||
const unit = t(UNIT_KEY[headline.display]);
|
||||
// Defensive: an unmapped display must never pass undefined to `t` (whose
|
||||
// resolver does `key.split(".")`). An empty unit is the honest degrade.
|
||||
const unitKey = UNIT_KEY[headline.display];
|
||||
const unit = unitKey ? t(unitKey) : "";
|
||||
const up = headline.delta >= 0;
|
||||
const dir = t(
|
||||
up
|
||||
@@ -151,7 +160,10 @@ export function CyclePhaseCrosstab({
|
||||
const metricLabel = t(
|
||||
METRIC_LABEL_KEY[row.metricKey] ?? row.metricKey,
|
||||
);
|
||||
const unit = t(UNIT_KEY[row.display]);
|
||||
// Defensive: an unmapped display must never pass undefined to `t`
|
||||
// (whose resolver does `key.split(".")`). Empty unit on a miss.
|
||||
const unitKey = UNIT_KEY[row.display];
|
||||
const unit = unitKey ? t(unitKey) : "";
|
||||
// `delta` = lutealAvg − follicularAvg: positive = the vital runs
|
||||
// higher in the luteal phase. The number stays NEUTRAL — higher is
|
||||
// good for steps but bad for resting HR, and this board is
|
||||
|
||||
Reference in new issue
Block a user