mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
Introduce a retrospective illness/condition journal: episodes with a lifecycle (acute / chronic-ongoing / recurring / flare) and a daily symptom timeline forked structurally from the cycle log-day surface. The journal is a record, not a medical device — no prediction or diagnosis UI. - Register `illness` as a born-gated (opt-in / default-off) module. The gate resolver flips the disabled-allowlist default to off for born-gated keys, so the surface stays dark until the account opts in from the Modules hub. `isIllnessEnabled` + `requireIllnessEnabled` delegate to the module foundation (one source of truth) and re-stamp an illness errorCode. - Episode CRUD under /api/illness: list / create / get / edit / resolve / delete, plus a per-episode-day day-log upsert on (episodeId, date). Every route wraps apiHandler, Zod-validates with returnAllZodIssues, narrows userId from auth, builds data field-by-field, and gates the module. Free-text notes encrypt at rest through the shared Bytes codec. - Log-day sheet (cycle fork): ~8 Jackson/WURSS symptom chips 0-3 + a functional-impact group + fever + an encrypted note, one row per day. - Episode history page + a born-gated nav entry, neutral palette, status via a neutral Badge, retrospective-only copy. i18n across all six locales. - Cover the new routes in the module-route-gate inventory; unit tests for the day-log upsert, the gate enforcement, and the born-gated resolver. The /api/auth/me module map now projects the `illness` flag (openapi.yaml regenerated).
100 lines
2.8 KiB
TypeScript
100 lines
2.8 KiB
TypeScript
/**
|
||
* Illness row → wire-DTO mappers (v1.18.1).
|
||
*
|
||
* The canonical `IllnessEpisodeDTO` and `IllnessDayLogDTO` shapes the iOS
|
||
* client mirrors (server-authoritative — iOS renders, never recomputes).
|
||
* Free-text notes live in `noteEncrypted` (Bytes) and are decrypted
|
||
* fail-soft on read (a key-rotation gap on one row reads `null`, never
|
||
* 500s the whole page). Symptom links flatten to `[{ key, severity }]`
|
||
* using the catalog key; `severity` carries the persisted per-link 0–3
|
||
* Jackson/WURSS intensity (NULL = a plain presence link).
|
||
*/
|
||
import { decryptFromBytes } from "@/lib/ai/coach/bytes-codec";
|
||
import type {
|
||
IllnessEpisode,
|
||
IllnessDayLog,
|
||
IllnessSymptom,
|
||
IllnessSymptomLink,
|
||
} from "@/generated/prisma/client";
|
||
|
||
export interface IllnessSymptomDTO {
|
||
key: string;
|
||
severity: number | null;
|
||
}
|
||
|
||
export interface IllnessEpisodeDTO {
|
||
id: string;
|
||
label: string;
|
||
type: string;
|
||
lifecycle: string;
|
||
onsetAt: string;
|
||
resolvedAt: string | null;
|
||
parentConditionId: string | null;
|
||
note: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface IllnessDayLogDTO {
|
||
id: string;
|
||
episodeId: string;
|
||
date: string;
|
||
functionalImpact: number | null;
|
||
feverC: number | null;
|
||
symptoms: IllnessSymptomDTO[];
|
||
note: string | null;
|
||
updatedAt: string;
|
||
}
|
||
|
||
/** Decrypt a Bytes note fail-soft (null on missing / undecryptable). */
|
||
function decryptNote(noteEncrypted: Uint8Array | null): string | null {
|
||
if (!noteEncrypted || noteEncrypted.byteLength === 0) return null;
|
||
try {
|
||
return decryptFromBytes(noteEncrypted);
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export function toIllnessEpisodeDTO(row: IllnessEpisode): IllnessEpisodeDTO {
|
||
return {
|
||
id: row.id,
|
||
label: row.label,
|
||
type: row.type,
|
||
lifecycle: row.lifecycle,
|
||
onsetAt: row.onsetAt.toISOString(),
|
||
resolvedAt: row.resolvedAt ? row.resolvedAt.toISOString() : null,
|
||
parentConditionId: row.parentConditionId,
|
||
note: decryptNote(row.noteEncrypted),
|
||
createdAt: row.createdAt.toISOString(),
|
||
updatedAt: row.updatedAt.toISOString(),
|
||
};
|
||
}
|
||
|
||
type DayLogWithLinks = IllnessDayLog & {
|
||
symptomLinks?: (IllnessSymptomLink & {
|
||
symptom: Pick<IllnessSymptom, "key">;
|
||
})[];
|
||
};
|
||
|
||
/** Prisma `include` that flattens the symptom links for the DTO mapper. */
|
||
export const dayLogSymptomInclude = {
|
||
symptomLinks: { include: { symptom: { select: { key: true } } } },
|
||
} as const;
|
||
|
||
export function toIllnessDayLogDTO(row: DayLogWithLinks): IllnessDayLogDTO {
|
||
return {
|
||
id: row.id,
|
||
episodeId: row.episodeId,
|
||
date: row.date,
|
||
functionalImpact: row.functionalImpact,
|
||
feverC: row.feverC,
|
||
symptoms: (row.symptomLinks ?? []).map((l) => ({
|
||
key: l.symptom.key,
|
||
severity: l.severity ?? null,
|
||
})),
|
||
note: decryptNote(row.noteEncrypted),
|
||
updatedAt: row.updatedAt.toISOString(),
|
||
};
|
||
}
|