fix: settle the new edit and HRV surfaces after a look at them

The HRV page's second chart reported its visible window into the same
stat-strip slot as the first, so the two replaced each other on every
render until React stopped it and the chart failed to load. Only the
primary chart reports the window now; the strip describes that measure.

The workout edit sheet no longer shows the "leave empty if you just
finished" hint, which is about logging and not about a stored start. On
the Apple Health card a type's name reads as the row's content above its
arrival line, with room between rows.
This commit is contained in:
Marc-André Bombeck committed 2026-10-08 09:52:51 +02:00
1 parent 196c331068
commit 1fe4a7af95
4 files changed
+46 -8

No files matched your search

@@ -22,11 +22,16 @@ vi.mock("@/hooks/use-ai-capability", () => ({
}));
vi.mock("next/dynamic", () => ({
default: () => {
const Stub = (props: { types?: string[]; title?: string }) => (
const Stub = (props: {
types?: string[];
title?: string;
onVisibleStats?: unknown;
}) => (
<div
data-slot="chart-stub"
data-types={(props.types ?? []).join(",")}
data-title={props.title ?? ""}
data-reports-stats={props.onVisibleStats ? "yes" : "no"}
/>
);
Stub.displayName = "HealthChartStub";
@@ -92,6 +97,24 @@ describe("/insights/hrv — SDNN and RMSSD kept apart (#1110)", () => {
expect(found[1]!.title).toMatch(/RMSSD$/);
});
it("lets only the primary chart write the stat strip's window", () => {
// Two charts writing the one stats slot replaced each other's window on
// every render: React's update-depth error, and the chart failed to load.
analyticsMock.mockReturnValue({
data: {
summaries: {
HEART_RATE_VARIABILITY: { count: 30 },
HRV_RMSSD: { count: 28 },
},
},
isEmpty: false,
});
const flags = [...render().matchAll(/data-reports-stats="(yes|no)"/g)].map(
(m) => m[1],
);
expect(flags).toEqual(["yes", "no"]);
});
it("keeps one unlabelled SDNN chart when there is no RMSSD", () => {
analyticsMock.mockReturnValue({
data: { summaries: { HEART_RATE_VARIABILITY: { count: 30 } } },
@@ -509,7 +509,9 @@ export function HealthKitMetricPage({
userTimezone={user?.timezone}
valueScale={resolvedScale}
valueOffset={resolvedOffset}
onVisibleStats={onVisibleStats}
// No `onVisibleStats`: the strip describes the primary measure, and
// two charts writing the one stats slot would replace each other's
// window on every render without end.
showDataTable
/>
) : null}
@@ -129,7 +129,7 @@ export function MetricFreshnessDisclosure<
</button>
{open && (
<ul id={panelId} className="space-y-1">
<ul id={panelId} className={detail ? "space-y-2.5" : "space-y-1"}>
{sorted.map((entry) => {
const extra = detail?.(entry);
return (
@@ -151,7 +151,16 @@ export function MetricFreshnessDisclosure<
className="size-3 shrink-0"
/>
) : null}
<span className="truncate">{label(entry.type)}</span>
{/* With a detail line under it, the type name is the
row's content and the line its meta. */}
<span
className={cn(
"truncate",
extra && !entry.stale && "text-foreground",
)}
>
{label(entry.type)}
</span>
</span>
<time
dateTime={entry.lastSeenAt}
@@ -309,16 +309,20 @@ export function ManualWorkoutForm({
max={wallClockNow(new Date(), timezone)}
aria-invalid={!!errors.start || undefined}
aria-describedby={[
`${ids.start}-hint`,
edit ? undefined : `${ids.start}-hint`,
describedBy("start", `${ids.start}-error`),
]
.filter(Boolean)
.join(" ")}
data-testid="manual-workout-start"
/>
<p id={`${ids.start}-hint`} className="text-muted-foreground text-xs">
{t("insights.workouts.manual.startHint")}
</p>
{/* "Leave empty if you just finished" is about logging; an edit
opens with the stored start in place. */}
{edit ? null : (
<p id={`${ids.start}-hint`} className="text-muted-foreground text-xs">
{t("insights.workouts.manual.startHint")}
</p>
)}
<FieldError id={`${ids.start}-error`} message={fieldError("start")} />
</div>