fix(cycle): gate the cycle ring tile and page redirect on the operator-aware module flag

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.
This commit is contained in:
Marc-André Bombeck committed 2026-06-16 01:19:54 +02:00
1 parent a73872435d
commit e974cdb5ff
2 files changed
+17 -9

No files matched your search

+10 -7
View File
@@ -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;
+7 -2
View File
@@ -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 ? <CycleRingTile /> : 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 ? <CycleRingTile /> : 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 ? (