diff --git a/src/app/cycle/page.tsx b/src/app/cycle/page.tsx
index dbdbe72d1..680ea2223 100644
--- a/src/app/cycle/page.tsx
+++ b/src/app/cycle/page.tsx
@@ -10,18 +10,21 @@ import { CycleView } from "@/components/cycle/cycle-view";
/**
* v1.15.0 — the cycle vertical entry.
*
- * Gated on `cycleTrackingEnabled` from `GET /api/auth/me` (resolved
- * server-side from gender + the per-user opt-in). An unauthenticated visitor
- * is bounced to login; an authenticated account without the feature enabled
- * is bounced home (the nav entry is already hidden for them, so this only
- * catches a direct URL hit). Every `/api/cycle/*` route also enforces the
- * gate server-side, so this is a UX redirect, not the security boundary.
+ * Gated on the resolved `modules.cycle` flag from `GET /api/auth/me`
+ * (v1.18.0 — the per-user toggle AND the operator server-wide kill-switch;
+ * the operator-unaware `cycleTrackingEnabled` field is NOT used here so an
+ * operator-disabled instance bounces a direct URL hit too). An
+ * unauthenticated visitor is bounced to login; an authenticated account
+ * without the module enabled is bounced home (the nav entry is already
+ * hidden for them, so this only catches a direct URL hit). Every
+ * `/api/cycle/*` route also enforces the gate server-side, so this is a UX
+ * redirect, not the security boundary.
*/
export default function CyclePage() {
const { user, isLoading, isAuthenticated } = useAuth();
const router = useRouter();
- const enabled = user?.cycleTrackingEnabled === true;
+ const enabled = user?.modules?.cycle !== false;
useEffect(() => {
if (isLoading) return;
diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx
index 4336534f5..2b13d97e3 100644
--- a/src/app/insights/page.tsx
+++ b/src/app/insights/page.tsx
@@ -404,11 +404,16 @@ export default function InsightsPage() {
// v1.15.3 — the cycle ring rides the scores strip as a gated sibling
// tile, only for a cycle-tracking account, so its calendar read never
// fires otherwise (the same `/api/auth/me` gate the sidebar nav uses).
- extraTile={user?.cycleTrackingEnabled ? : undefined}
+ // v1.18.0 — gate on the resolved `modules.cycle` flag (per-user toggle
+ // AND the operator server-wide kill-switch) so an operator-off
+ // instance never renders the ring nor fires its calendar read.
+ extraTile={
+ user?.modules?.cycle === true ? : undefined
+ }
// v1.15.5 — when the cycle ring is shown it TAKES the Strain slot:
// hide Strain so the strip stays compact instead of growing a sixth
// tile. Strain stays visible for non-cycle accounts.
- hideStrain={user?.cycleTrackingEnabled === true}
+ hideStrain={user?.modules?.cycle === true}
/>
),
"daily-briefing": flags.briefing ? (