From e974cdb5ff8b474fb23cc7f3ba4b867f8614669e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 16 Jun 2026 01:19:54 +0200 Subject: [PATCH] fix(cycle): gate the cycle ring tile and page redirect on the operator-aware module flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The insights cycle ring tile and the /cycle page redirect gated on user.cycleTrackingEnabled, which /api/auth/me resolves without the operator availability layer — so with the Cycle module disabled server-wide the ring still rendered (and fired its calendar read) and a direct /cycle URL still loaded. Read user.modules.cycle instead, which the /me map already AND-s with the operator switch, so an operator-off instance suppresses both surfaces. --- src/app/cycle/page.tsx | 17 ++++++++++------- src/app/insights/page.tsx | 9 +++++++-- 2 files changed, 17 insertions(+), 9 deletions(-) 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 ? (