mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(sleep-score): publish the rhythm floor and name it in the few-nights note
The note hard-coded "of 3" in every locale. SleepScoreValue now carries rhythmMinNights, and the sheet reads the floor from it.
This commit is contained in:
1 parent
9fe6f98fdb
commit
248b5dfd6b
11 files changed
+83
-8
No files matched your search
+1
-1
@@ -4397,7 +4397,7 @@
|
||||
"composition": "Zusammensetzung"
|
||||
},
|
||||
"gap": {
|
||||
"fewNights": "noch nicht genug Nächte ({count} von 3)",
|
||||
"fewNights": "noch nicht genug Nächte ({count} von {needed})",
|
||||
"noMainSleep": "letzter Tag nur mit Nickerchen"
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4397,7 +4397,7 @@
|
||||
"composition": "Composition"
|
||||
},
|
||||
"gap": {
|
||||
"fewNights": "not enough nights yet ({count} of 3)",
|
||||
"fewNights": "not enough nights yet ({count} of {needed})",
|
||||
"noMainSleep": "latest day had only a nap"
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4397,7 +4397,7 @@
|
||||
"composition": "Composición"
|
||||
},
|
||||
"gap": {
|
||||
"fewNights": "aún no hay suficientes noches ({count} de 3)",
|
||||
"fewNights": "aún no hay suficientes noches ({count} de {needed})",
|
||||
"noMainSleep": "el último día solo tuvo una siesta"
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4397,7 +4397,7 @@
|
||||
"composition": "Composition"
|
||||
},
|
||||
"gap": {
|
||||
"fewNights": "pas encore assez de nuits ({count} sur 3)",
|
||||
"fewNights": "pas encore assez de nuits ({count} sur {needed})",
|
||||
"noMainSleep": "le dernier jour n'a eu qu'une sieste"
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4397,7 +4397,7 @@
|
||||
"composition": "Composizione"
|
||||
},
|
||||
"gap": {
|
||||
"fewNights": "notti ancora insufficienti ({count} su 3)",
|
||||
"fewNights": "notti ancora insufficienti ({count} su {needed})",
|
||||
"noMainSleep": "l'ultimo giorno solo un pisolino"
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4397,7 +4397,7 @@
|
||||
"composition": "구성"
|
||||
},
|
||||
"gap": {
|
||||
"fewNights": "아직 밤이 부족함 ({count}/3)",
|
||||
"fewNights": "아직 밤이 부족함 ({count}/{needed})",
|
||||
"noMainSleep": "최근 하루는 낮잠만 있음"
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4397,7 +4397,7 @@
|
||||
"composition": "Struktura"
|
||||
},
|
||||
"gap": {
|
||||
"fewNights": "za mało nocy ({count} z 3)",
|
||||
"fewNights": "za mało nocy ({count} z {needed})",
|
||||
"noMainSleep": "ostatni dzień tylko z drzemką"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
/**
|
||||
* A rhythm sub-score withheld for too few nights names the floor the server
|
||||
* published (`rhythmMinNights`), not a number the sheet carries itself.
|
||||
*/
|
||||
|
||||
const anatomy = vi.fn();
|
||||
vi.mock("../score-anatomy-view", () => ({
|
||||
ScoreAnatomyView: (props: Record<string, unknown>) => {
|
||||
anatomy(props);
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
vi.mock("@/components/insights/insight-status-card", () => ({
|
||||
InsightStatusCard: () => null,
|
||||
}));
|
||||
vi.mock("../score-history", () => ({
|
||||
ScoreHistoryChart: () => null,
|
||||
ScoreTrendChartDynamic: () => null,
|
||||
}));
|
||||
vi.mock("@/lib/i18n/context", () => ({
|
||||
useTranslations: () => ({
|
||||
t: (key: string, vars?: Record<string, string | number>) =>
|
||||
vars ? `${key}:${JSON.stringify(vars)}` : key,
|
||||
}),
|
||||
}));
|
||||
vi.mock("../use-derived-metric", () => ({
|
||||
useDerivedMetric: () => ({
|
||||
isLoading: false,
|
||||
data: {
|
||||
status: "ok",
|
||||
value: {
|
||||
score: 70,
|
||||
band: "yellow",
|
||||
subScores: [
|
||||
{ key: "consistency", value: null, weight: 0.2, gap: "few_nights" },
|
||||
],
|
||||
rhythmNights: 2,
|
||||
rhythmMinNights: 5,
|
||||
},
|
||||
coverage: 1,
|
||||
confidence: "high",
|
||||
provenance: {},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
import { CompositeScoreAnatomy } from "../composite-score-anatomy";
|
||||
|
||||
describe("sleep score sheet — the few-nights gap", () => {
|
||||
it("names the published floor", () => {
|
||||
renderToStaticMarkup(<CompositeScoreAnatomy metric="SLEEP_SCORE" />);
|
||||
const props = anatomy.mock.calls.at(-1)![0] as {
|
||||
contributors: Array<{ key: string; missingLabel?: string }>;
|
||||
};
|
||||
const consistency = props.contributors.find((c) => c.key === "consistency");
|
||||
expect(consistency?.missingLabel).toContain('"count":2');
|
||||
expect(consistency?.missingLabel).toContain('"needed":5');
|
||||
});
|
||||
});
|
||||
@@ -243,6 +243,7 @@ export function CompositeScoreAnatomy({
|
||||
s.gap === "few_nights"
|
||||
? t("insights.derived.composite.SLEEP_SCORE.gap.fewNights", {
|
||||
count: v.rhythmNights ?? 0,
|
||||
needed: v.rhythmMinNights,
|
||||
})
|
||||
: s.gap === "no_main_sleep"
|
||||
? t("insights.derived.composite.SLEEP_SCORE.gap.noMainSleep")
|
||||
|
||||
@@ -115,6 +115,11 @@ export interface SleepScoreValue {
|
||||
* `RHYTHM_MIN_NIGHTS` both are withheld with `gap: "few_nights"`.
|
||||
*/
|
||||
rhythmNights: number;
|
||||
/**
|
||||
* `RHYTHM_MIN_NIGHTS`, published so a client names the floor it shows
|
||||
* ("2 of 3 nights") from the server instead of carrying its own copy.
|
||||
*/
|
||||
rhythmMinNights: number;
|
||||
/**
|
||||
* v1.42 — the score of every scorable night in the window (oldest → newest,
|
||||
* capped to `SPARKLINE_MAX_POINTS`), so a surface can show the score's
|
||||
@@ -640,6 +645,7 @@ export async function computeSleepScore(
|
||||
subScores,
|
||||
windowNights: scorableNights.length,
|
||||
rhythmNights: midpoints.length,
|
||||
rhythmMinNights: RHYTHM_MIN_NIGHTS,
|
||||
series,
|
||||
},
|
||||
coverage,
|
||||
|
||||
@@ -325,7 +325,7 @@ export const sleepScoreValue = z
|
||||
.enum(["few_nights", "no_main_sleep"])
|
||||
.optional()
|
||||
.describe(
|
||||
"Only on a null `consistency` / `timing`: why it is withheld. `few_nights` — fewer than three nights in the window have a main sleep on the clock (count in `rhythmNights`); `no_main_sleep` — the latest day had only a daytime nap. Absent otherwise.",
|
||||
"Only on a null `consistency` / `timing`: why it is withheld. `few_nights` — fewer than `rhythmMinNights` nights in the window have a main sleep on the clock (count in `rhythmNights`); `no_main_sleep` — the latest day had only a daytime nap. Absent otherwise.",
|
||||
),
|
||||
}),
|
||||
)
|
||||
@@ -337,6 +337,12 @@ export const sleepScoreValue = z
|
||||
.describe(
|
||||
"Nights in the window with a main sleep placed on the clock — what consistency and timing are computed from. A daytime nap is not one; a short night is.",
|
||||
),
|
||||
rhythmMinNights: z
|
||||
.number()
|
||||
.int()
|
||||
.describe(
|
||||
'How many `rhythmNights` consistency and timing need before they are scored; below it they carry `gap: "few_nights"`.',
|
||||
),
|
||||
series: z
|
||||
.array(z.number())
|
||||
.describe(
|
||||
|
||||
Reference in new issue
Block a user