mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
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:
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 ? (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user