mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
feat(mental-health): gate the PHQ-9/GAD-7 check-in behind an opt-in module
The mental-wellbeing screener surface shipped built but unreachable: no nav entry, no module key, ungated. Add a `mentalHealth` module key (opt-in, default OFF — a depression/anxiety self-assessment is at least as sensitive as mood) to the registry, the module-hub icon map, and the exhaustive module maps. Give the check-in a nav home at /insights/mental-wellbeing, born-gated on the module so it appears only once the account opts in, and redirect the page home on a direct hit when the module is off. Item answers stay encrypted and off the AI Coach / MCP regardless of the toggle; this only controls whether the surface is visible and reachable.
This commit is contained in:
1 parent
15e740bc2b
commit
f62ba50ff0
7 files changed
+88
-7
No files matched your search
@@ -1,16 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { MentalWellbeing } from "@/components/mental-health/mental-wellbeing";
|
||||
|
||||
/**
|
||||
* v1.25 — `/insights/mental-wellbeing`.
|
||||
* v1.25.0 — `/insights/mental-wellbeing`.
|
||||
*
|
||||
* Opt-in PHQ-9 / GAD-7 screener surface, beside mood tracking. The screen owns
|
||||
* its own questionnaire + result + crisis-resource flow; it deliberately does
|
||||
* NOT mount the shared Coach-launch assessment card (mental-health item content
|
||||
* is kept out of the AI Coach by construction).
|
||||
*
|
||||
* Born-gated on the resolved `modules.mentalHealth` 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). Both assessment
|
||||
* routes also enforce the gate server-side, so this is a UX redirect, not the
|
||||
* security boundary.
|
||||
*/
|
||||
export default function MentalWellbeingPage() {
|
||||
const { user, isLoading, isAuthenticated } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const enabled = user?.modules?.mentalHealth === 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 (
|
||||
<div className="p-4 sm:p-6">
|
||||
<MentalWellbeing />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Activity,
|
||||
Bell,
|
||||
Brain,
|
||||
Droplets,
|
||||
FileScan,
|
||||
FlaskConical,
|
||||
@@ -82,6 +83,19 @@ export const NAV_DESTINATIONS: ReadonlyArray<NavDestination> = [
|
||||
tourId: "nav-mood",
|
||||
requiresModule: "mood",
|
||||
},
|
||||
// v1.25.0 — opt-in mental-health screeners (PHQ-9 / GAD-7), beside mood.
|
||||
// Born-gated: `requiresModule: "mentalHealth"` reads the opt-in (default-off)
|
||||
// key from the resolved module map, so the entry is absent until the account
|
||||
// turns the module on from the Modules hub. The destination is the dedicated
|
||||
// `/insights/mental-wellbeing` check-in surface (its own nav home — the
|
||||
// active-detection helper defers `/insights` to it on that path).
|
||||
{
|
||||
href: "/insights/mental-wellbeing",
|
||||
tKey: "nav.mentalWellbeing",
|
||||
icon: Brain,
|
||||
tourId: "nav-mental-wellbeing",
|
||||
requiresModule: "mentalHealth",
|
||||
},
|
||||
{
|
||||
href: "/cycle",
|
||||
tKey: "nav.cycle",
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
Activity,
|
||||
Award,
|
||||
Blocks,
|
||||
Brain,
|
||||
CalendarHeart,
|
||||
Droplet,
|
||||
CloudSun,
|
||||
@@ -80,6 +81,8 @@ const MODULE_ICONS: Record<ModuleKey, LucideIcon> = {
|
||||
mcp: Plug,
|
||||
// v1.25.0 (W-DOCS-IN) — inbound clinical documents (opt-in).
|
||||
inboundDocuments: FileScan,
|
||||
// v1.25.0 — opt-in mental-health screeners (PHQ-9 / GAD-7).
|
||||
mentalHealth: Brain,
|
||||
};
|
||||
|
||||
export function ModulesSection() {
|
||||
|
||||
@@ -86,6 +86,7 @@ function moduleMap(
|
||||
environment: true,
|
||||
mcp: true,
|
||||
inboundDocuments: true,
|
||||
mentalHealth: true,
|
||||
};
|
||||
return { ...base, ...overrides };
|
||||
}
|
||||
|
||||
@@ -352,12 +352,19 @@ describe("resolveModuleEnabled — mcp module (opt-in, default-OFF)", () => {
|
||||
});
|
||||
|
||||
describe("registry — opt-in marker", () => {
|
||||
// The opt-in modules ship dark because they open an egress/attack surface:
|
||||
// `mcp` (remote external-assistant endpoint), `environment` (outbound weather
|
||||
// fetch tied to a coarse location), and `inboundDocuments` (sends an uploaded
|
||||
// clinical document to the OCR/vision provider). Every other module is the
|
||||
// default-on disabled-allowlist.
|
||||
const OPT_IN_KEYS = new Set(["mcp", "environment", "inboundDocuments"]);
|
||||
// The opt-in modules ship dark because they open an egress/attack surface or
|
||||
// are highly sensitive: `mcp` (remote external-assistant endpoint),
|
||||
// `environment` (outbound weather fetch tied to a coarse location),
|
||||
// `inboundDocuments` (sends an uploaded clinical document to the OCR/vision
|
||||
// provider), and `mentalHealth` (a depression / anxiety self-assessment — at
|
||||
// least as sensitive as mood, surfaced only on explicit opt-in). Every other
|
||||
// module is the default-on disabled-allowlist.
|
||||
const OPT_IN_KEYS = new Set([
|
||||
"mcp",
|
||||
"environment",
|
||||
"inboundDocuments",
|
||||
"mentalHealth",
|
||||
]);
|
||||
|
||||
it("marks only the egress-surface modules opt-in (every other is default-on)", () => {
|
||||
for (const key of MODULE_KEYS) {
|
||||
|
||||
@@ -136,6 +136,14 @@ export const MODULE_KEYS = [
|
||||
// `optIn` marker on its registry entry inverts the per-user default;
|
||||
// everything else reuses the existing module machinery unchanged.
|
||||
"inboundDocuments",
|
||||
// v1.25.0 — opt-in mental-health screeners (PHQ-9 / GAD-7), beside mood
|
||||
// tracking. OPT-IN (off by default): a depression / anxiety self-assessment
|
||||
// is at least as sensitive as mood, so it ships dark and the user turns it on
|
||||
// deliberately. The `optIn` marker inverts the per-user default; with it off
|
||||
// the `/insights/mental-wellbeing` surface is hidden from nav and the
|
||||
// assessment routes refuse server-side. Item answers are always encrypted and
|
||||
// always excluded from the AI Coach / MCP regardless of this toggle.
|
||||
"mentalHealth",
|
||||
] as const;
|
||||
|
||||
export type ModuleKey = (typeof MODULE_KEYS)[number];
|
||||
@@ -298,6 +306,18 @@ export const MODULE_REGISTRY: Readonly<Record<ModuleKey, ModuleDefinition>> =
|
||||
// configured OCR/vision provider, so the user turns it on deliberately.
|
||||
optIn: true,
|
||||
},
|
||||
mentalHealth: {
|
||||
key: "mentalHealth",
|
||||
labelKey: "modules.mentalHealth.label",
|
||||
descriptionKey: "modules.mentalHealth.description",
|
||||
category: "tracking",
|
||||
// Off by default: a depression / anxiety screener is highly sensitive, so
|
||||
// the surface ships dark and the user opts in deliberately. Turning it on
|
||||
// reveals the `/insights/mental-wellbeing` check-in and lets the screener
|
||||
// total ride the doctor-report export; item answers stay encrypted and
|
||||
// off the AI Coach / MCP regardless.
|
||||
optIn: true,
|
||||
},
|
||||
});
|
||||
|
||||
const MODULE_KEY_SET: ReadonlySet<string> = new Set(MODULE_KEYS);
|
||||
|
||||
@@ -320,6 +320,7 @@ describe("doctor-report — module enable/disable gating", () => {
|
||||
environment: true,
|
||||
mcp: true,
|
||||
inboundDocuments: true,
|
||||
mentalHealth: true,
|
||||
} as const;
|
||||
|
||||
it("includes every module's data when all modules are enabled", async () => {
|
||||
|
||||
Reference in new issue
Block a user