mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +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).
55 lines
2.1 KiB
TypeScript
55 lines
2.1 KiB
TypeScript
/**
|
|
* Illness / condition-journal feature gate (v1.18.1).
|
|
*
|
|
* The illness journal is BORN-GATED (opt-in / default-off) and resolves
|
|
* its enabled-state through the module foundation — there is no per-domain
|
|
* profile row and no second source of truth. `isIllnessEnabled` and the
|
|
* `requireIllnessEnabled` 403 guard both delegate to the module gate's
|
|
* `isModuleEnabled(userId, "illness")`, which (for a born-gated key) reads
|
|
* an explicit `modulePreferencesJson.illness === true` AND the operator
|
|
* server-wide availability. This mirrors `src/lib/cycle/gate.ts` so every
|
|
* `/api/illness/*` route gates identically.
|
|
*/
|
|
import { apiError } from "@/lib/api-response";
|
|
import { isModuleEnabled, requireModuleEnabled } from "@/lib/modules/gate";
|
|
|
|
/** Wire `errorCode` the iOS retry classifier branches on. */
|
|
export const ILLNESS_DISABLED_ERROR_CODE = "illness.disabled";
|
|
|
|
/**
|
|
* Fully-resolved illness availability for an account: the born-gated opt-in
|
|
* AND the operator server-wide kill-switch, via the module foundation.
|
|
*/
|
|
export function isIllnessEnabled(userId: string): Promise<boolean> {
|
|
return isModuleEnabled(userId, "illness");
|
|
}
|
|
|
|
/**
|
|
* Route-guard outcome: a clear pass or a ready 403 envelope to return
|
|
* verbatim — mirrors `CycleGateResult`.
|
|
*/
|
|
export type IllnessGateResult =
|
|
| { enabled: true }
|
|
| { enabled: false; response: Response };
|
|
|
|
/**
|
|
* Enforce the gate for an `/api/illness/*` route. Returns a 403
|
|
* `illness.disabled` envelope when the account has not opted in (or the
|
|
* operator turned the module off server-wide) — even with a valid Bearer
|
|
* token. Delegates to `requireModuleEnabled` but re-stamps the
|
|
* illness-specific `errorCode` so the iOS classifier branches cleanly.
|
|
*/
|
|
export async function requireIllnessEnabled(
|
|
userId: string,
|
|
): Promise<IllnessGateResult> {
|
|
const gate = await requireModuleEnabled(userId, "illness");
|
|
if (gate.enabled) return { enabled: true };
|
|
return {
|
|
enabled: false,
|
|
response: apiError("Illness journal is not enabled", 403, {
|
|
errorCode: ILLNESS_DISABLED_ERROR_CODE,
|
|
module: "illness",
|
|
}),
|
|
};
|
|
}
|