feat(illness): add the condition-journal MVP behind a born-gated module

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).
This commit is contained in:
Marc-André Bombeck committed 2026-06-16 21:09:35 +02:00
1 parent bee7295b19
commit b079431a8d
34 files changed
+2799 -6

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { Loader2 } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
import { IllnessView } from "@/components/illness/illness-view";
/**
* v1.18.1 — the illness / condition-journal entry.
*
* Born-gated on the resolved `modules.illness` flag from `GET /api/auth/me`
* (opt-in / default-off; the per-user opt-in AND the operator server-wide
* kill-switch). An unauthenticated visitor is bounced to login; an
* authenticated account without the module opted in is bounced home (the
* nav entry is already hidden for them, so this only catches a direct URL
* hit). Every `/api/illness/*` route also enforces the gate server-side,
* so this is a UX redirect, not the security boundary.
*/
export default function IllnessPage() {
const { user, isLoading, isAuthenticated } = useAuth();
const router = useRouter();
// Born-gated: enabled ONLY when the resolved map says `true`.
const enabled = user?.modules?.illness === true;
useEffect(() => {
if (isLoading) return;
if (!isAuthenticated) {
router.push("/auth/login");
} else if (!enabled) {
router.push("/");
}
}, [isLoading, isAuthenticated, enabled, router]);
if (isLoading || !isAuthenticated || !enabled) {
return (
<div className="flex h-64 items-center justify-center">
<Loader2 className="text-primary h-8 w-8 animate-spin motion-reduce:animate-none" />
</div>
);
}
return <IllnessView />;
}