feat(polish): default tracking modules on; illness backdate + course fixes

A clean install now defaults every tracking module on (weight, blood
pressure, glucose, sleep, mood, medications, labs, documents, insights,
mental wellbeing, cycle, illness, environment, workouts) so the app is
usable without enabling each surface by hand. Only the two channels that
open an egress or attack surface stay opt-in — the remote MCP endpoint and
the micronutrient sync; the document AI-read and the daily-briefing push
remain separately gated and off. The change touches defaults only:
accounts with a saved preference resolve exactly as before, the new default
applies solely where no preference exists.

Drop the "typical range = median ± MAD" method caption from the vitals
baseline tiles — it repeated across every tile and read as out of place at
the top of the vitals surface. The dedicated bands and composite scores
keep their method copy.

Illness episodes: the capture sheet now carries an editable end date
alongside the start, so a whole past episode can be recorded after the
fact and an open-ended course can be resolved (or re-opened) later. The
end is bounded to on-or-after the start.

Fix the per-episode "how it progressed" card rendering an empty body when
the engine returned a valid result with no notable findings — it now shows
a calm, honest line instead of looking as though nothing was recorded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018V8V9qapgHi7fhgtMJ8SkM
This commit is contained in:
Marc-André BombeckandClaude Opus 4.8 committed 2026-07-17 11:02:57 +02:00
1 parent f6e6593bcb
commit 2da6a192a7
12 files changed
+172 -80

No files matched your search

@@ -57,6 +57,25 @@ function CorrelationBody({ value }: { value: IllnessCorrelationValue }) {
// the engine on thin data — never a recovery claim, never a second gap.
const sleep = value.sleepContext;
// The engine can return `ok` with NO findings — a mild episode where nothing
// strayed notably from the personal band, still active with no recovery gap,
// and no red flag. Without this the card body rendered empty under its "how
// it progressed" heading, reading as if nothing was recorded at all. Show a
// calm, honest line instead (retrospective, never a fabricated finding).
const hasContent =
value.redFlags.length > 0 ||
gap !== null ||
value.preOnset.length > 0 ||
value.nadir.length > 0;
if (!hasContent) {
return (
<p className="text-muted-foreground text-sm">
{t("illness.correlation.settled")}
</p>
);
}
return (
<div className="space-y-5">
{value.redFlags.length > 0 ? (
+46 -4
View File
@@ -83,6 +83,10 @@ export function NewEpisodeSheet({
const [type, setType] = useState<IllnessType>("INFECTION");
const [lifecycle, setLifecycle] = useState<IllnessLifecycle>("ACUTE");
const [onset, setOnset] = useState(today);
// Resolved/end date — "" means the episode is still ongoing (open-ended).
// Backdating both onset and this end date lets a whole past episode be
// recorded after the fact.
const [resolved, setResolved] = useState("");
const [parentId, setParentId] = useState<string>(NONE);
const [note, setNote] = useState("");
@@ -96,6 +100,9 @@ export function NewEpisodeSheet({
setType(editEpisode?.type ?? "INFECTION");
setLifecycle(editEpisode?.lifecycle ?? "ACUTE");
setOnset(editEpisode ? editEpisode.onsetAt.slice(0, 10) : today);
setResolved(
editEpisode?.resolvedAt ? editEpisode.resolvedAt.slice(0, 10) : "",
);
setParentId(editEpisode?.parentConditionId ?? NONE);
setNote(editEpisode?.note ?? "");
} else if (!open && wasOpen) {
@@ -104,9 +111,17 @@ export function NewEpisodeSheet({
const showParent = PARENTABLE.includes(lifecycle);
// The end date can never precede the start; the field is min-bounded to the
// onset, and this guard belts-and-braces the same invariant the server holds.
const endBeforeStart = resolved !== "" && resolved < onset;
async function handleSave() {
if (label.trim() === "") return;
if (label.trim() === "" || endBeforeStart) return;
const onsetAt = new Date(`${onset}T12:00:00`).toISOString();
// "" ⇒ ongoing (null clears any stored end). Anchor to local noon so the
// UTC instant lands on the intended calendar day regardless of offset.
const resolvedAt =
resolved === "" ? null : new Date(`${resolved}T12:00:00`).toISOString();
const parentConditionId = showParent && parentId !== NONE ? parentId : null;
try {
if (isEdit && editEpisode) {
@@ -117,6 +132,7 @@ export function NewEpisodeSheet({
type,
lifecycle,
onsetAt,
resolvedAt,
parentConditionId,
note: note.trim() === "" ? null : note,
},
@@ -126,9 +142,8 @@ export function NewEpisodeSheet({
label: label.trim(),
type,
lifecycle,
// Anchor to local noon so the UTC instant lands on the intended
// calendar day regardless of the viewer's offset.
onsetAt,
resolvedAt,
parentConditionId,
note: note.trim() === "" ? null : note,
});
@@ -154,7 +169,7 @@ export function NewEpisodeSheet({
</Button>
<Button
onClick={handleSave}
disabled={pending || label.trim() === ""}
disabled={pending || label.trim() === "" || endBeforeStart}
>
{t("common.save")}
</Button>
@@ -240,6 +255,33 @@ export function NewEpisodeSheet({
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="illness-resolved">{t("illness.new.resolved")}</Label>
<div className="flex items-center gap-2">
<DateField
id="illness-resolved"
value={resolved}
min={onset}
max={today}
onChange={setResolved}
className="max-w-44"
/>
{resolved !== "" ? (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setResolved("")}
>
{t("illness.new.markOngoing")}
</Button>
) : null}
</div>
<p className="text-muted-foreground text-xs">
{t("illness.new.resolvedHelp")}
</p>
</div>
<div className="space-y-1.5">
<Label htmlFor="illness-note">{t("illness.new.note")}</Label>
<Textarea
+3
View File
@@ -59,6 +59,9 @@ export interface IllnessEpisodeCreateInput {
type: IllnessType;
lifecycle?: IllnessLifecycle;
onsetAt?: string;
/** Optional resolved/end instant — set to backdate an already-recovered
* episode captured after the fact; omit/null for an ongoing episode. */
resolvedAt?: string | null;
parentConditionId?: string | null;
note?: string | null;
}
@@ -288,7 +288,14 @@ function BaselineTile({
framing={framing}
directionSentiment={vitalSentiment(type)}
provenance={
<MetricProvenance metric={metric} provenance={data.provenance} />
// v1.29.1 — the VITALS_BASELINE "typical range = median ± MAD" method
// caption is suppressed on its own tiles: it repeated across every
// vital baseline tile and read as out-of-place clutter at the top of
// the vitals surface. The dedicated bands (wrist-temp, stair speed,
// 6-min walk) keep their method copy — only VITALS_BASELINE is muted.
metric === "VITALS_BASELINE" ? undefined : (
<MetricProvenance metric={metric} provenance={data.provenance} />
)
}
footer={<LearnMoreLink concept={tileId} />}
/>