diff --git a/src/app/insights/mental-wellbeing/page.tsx b/src/app/insights/mental-wellbeing/page.tsx
index fb6f1039d..778255ddf 100644
--- a/src/app/insights/mental-wellbeing/page.tsx
+++ b/src/app/insights/mental-wellbeing/page.tsx
@@ -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 (
+
+
+
+ );
+ }
+
return (
diff --git a/src/components/layout/nav-model.ts b/src/components/layout/nav-model.ts
index 01d81ad3a..49e04b45b 100644
--- a/src/components/layout/nav-model.ts
+++ b/src/components/layout/nav-model.ts
@@ -1,6 +1,7 @@
import {
Activity,
Bell,
+ Brain,
Droplets,
FileScan,
FlaskConical,
@@ -82,6 +83,19 @@ export const NAV_DESTINATIONS: ReadonlyArray = [
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",
diff --git a/src/components/settings/modules-section.tsx b/src/components/settings/modules-section.tsx
index 3dfede292..e67da7e3f 100644
--- a/src/components/settings/modules-section.tsx
+++ b/src/components/settings/modules-section.tsx
@@ -28,6 +28,7 @@ import {
Activity,
Award,
Blocks,
+ Brain,
CalendarHeart,
Droplet,
CloudSun,
@@ -80,6 +81,8 @@ const MODULE_ICONS: Record = {
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() {
diff --git a/src/lib/dashboard/__tests__/snapshot.test.ts b/src/lib/dashboard/__tests__/snapshot.test.ts
index 0e4b2baed..0cf95f422 100644
--- a/src/lib/dashboard/__tests__/snapshot.test.ts
+++ b/src/lib/dashboard/__tests__/snapshot.test.ts
@@ -86,6 +86,7 @@ function moduleMap(
environment: true,
mcp: true,
inboundDocuments: true,
+ mentalHealth: true,
};
return { ...base, ...overrides };
}
diff --git a/src/lib/modules/__tests__/gate.test.ts b/src/lib/modules/__tests__/gate.test.ts
index 944386804..730bf8b87 100644
--- a/src/lib/modules/__tests__/gate.test.ts
+++ b/src/lib/modules/__tests__/gate.test.ts
@@ -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) {
diff --git a/src/lib/modules/registry.ts b/src/lib/modules/registry.ts
index 663b32043..0fae157e5 100644
--- a/src/lib/modules/registry.ts
+++ b/src/lib/modules/registry.ts
@@ -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> =
// 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 = new Set(MODULE_KEYS);
diff --git a/tests/integration/doctor-report-sections.test.ts b/tests/integration/doctor-report-sections.test.ts
index 0f2732235..c3999f6ae 100644
--- a/tests/integration/doctor-report-sections.test.ts
+++ b/tests/integration/doctor-report-sections.test.ts
@@ -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 () => {