From 7df260d62ec3061e10c286ca9f14098a8aece6e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 24 Sep 2026 10:17:00 +0200 Subject: [PATCH] fix(daily): send the morning briefing push with AI analysis off The push refused outright while the `insights` module was off, which now means only "AI analysis". The digest it reads already drops the model-written lead and top signal in that case, so the push goes out with the deterministic line (the score, or the day's items) and a digest with nothing to say is still skipped. The `module-off` outcome and its counter are gone. Refs #975 --- .../__tests__/daily-briefing-push.test.ts | 19 +++++++++++---- src/lib/daily/daily-briefing-push.ts | 24 +++++++------------ src/lib/jobs/daily-briefing.ts | 5 ---- src/lib/jobs/reminder/register-status.ts | 1 - 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/lib/daily/__tests__/daily-briefing-push.test.ts b/src/lib/daily/__tests__/daily-briefing-push.test.ts index 151acdc14..41b3533d5 100644 --- a/src/lib/daily/__tests__/daily-briefing-push.test.ts +++ b/src/lib/daily/__tests__/daily-briefing-push.test.ts @@ -82,7 +82,6 @@ function makeDeps( return { dispatch, loadDigest: async () => makeDigest(), - isModuleEnabled: async () => true, ...over, } as DailyBriefingDispatchDeps & { dispatch: ReturnType }; } @@ -253,17 +252,27 @@ describe("maybeDispatchDailyBriefing", () => { expect(deps.dispatch).not.toHaveBeenCalled(); }); - it("insights module off → no dispatch", async () => { + it("AI analysis off: the push still goes out with the deterministic line", async () => { + // The digest the loader returns when the briefing capability is + // unavailable: no lead, no top signal, the score floor as the line. const prisma = makePrisma({}); - const deps = makeDeps({ isModuleEnabled: async () => false }); + const deps = makeDeps({ + loadDigest: async () => + makeDigest({ + briefingLead: null, + topSignal: null, + line: "Your health score today is 82.", + }), + }); const result = await maybeDispatchDailyBriefing( prisma, "u1", IN_WINDOW, deps, ); - expect(result).toBe("module-off"); - expect(deps.dispatch).not.toHaveBeenCalled(); + expect(result).toBe("sent"); + const payload = JSON.stringify(deps.dispatch.mock.calls[0]); + expect(payload).toContain("Your health score today is 82."); }); it("frequency cap: an ok ledger row earlier the SAME local day suppresses the second push", async () => { diff --git a/src/lib/daily/daily-briefing-push.ts b/src/lib/daily/daily-briefing-push.ts index 9cef2f919..ef62c1f24 100644 --- a/src/lib/daily/daily-briefing-push.ts +++ b/src/lib/daily/daily-briefing-push.ts @@ -32,7 +32,6 @@ import type { Locale } from "@/lib/i18n/config"; import { resolveJobLocale } from "@/lib/i18n/job-locale"; import { getServerTranslator } from "@/lib/i18n/server-translator"; import type { ServerTranslator } from "@/lib/i18n/server-translator"; -import { isModuleEnabled } from "@/lib/modules/gate"; import { wallClockInTz } from "@/lib/tz/wall-clock"; import { userDayKey } from "@/lib/tz/format"; import { dispatchNotification } from "@/lib/notifications/dispatcher"; @@ -72,7 +71,6 @@ export type DailyBriefingDispatchResult = | "suppressed-frequency" | "no-digest" | "opted-out" - | "module-off" | "outside-window" | "no-channel" | "missing-user" @@ -81,7 +79,6 @@ export type DailyBriefingDispatchResult = export interface DailyBriefingDispatchDeps { dispatch?: typeof dispatchNotification; loadDigest?: (user: User, now: Date, locale: Locale) => Promise; - isModuleEnabled?: typeof isModuleEnabled; } function loadDailyBriefingDigest( @@ -137,10 +134,15 @@ export function buildDailyBriefingPush( * 1. user missing → `missing-user` * 2. no opt-in (default OFF) → `opted-out` (no enabled DAILY_BRIEFING pref) * 3. outside morning window → `outside-window` - * 4. insights module off → `module-off` - * 5. already pushed today → `suppressed-frequency` (push_attempts ledger) - * 6. digest not substantive → `no-digest` - * 7. dispatch; no channel → `no-channel`; else `sent`. + * 4. already pushed today → `suppressed-frequency` (push_attempts ledger) + * 5. digest not substantive → `no-digest` + * 6. dispatch; no channel → `no-channel`; else `sent`. + * + * No module gate. The `insights` module is the AI analysis opt-out: with it + * off (or AI unavailable for any other reason) the digest carries no briefing + * lead or top signal, and the push body falls to the digest's deterministic + * line (the score, or the all-clear). A digest with nothing deterministic to + * say is skipped as `no-digest`, like any other. */ export async function maybeDispatchDailyBriefing( prisma: PrismaClient, @@ -150,7 +152,6 @@ export async function maybeDispatchDailyBriefing( ): Promise { const dispatch = deps.dispatch ?? dispatchNotification; const loadDigest = deps.loadDigest ?? loadDailyBriefingDigest; - const moduleGate = deps.isModuleEnabled ?? isModuleEnabled; try { const user = await prisma.user.findUnique({ where: { id: userId } }); @@ -187,13 +188,6 @@ export async function maybeDispatchDailyBriefing( return "outside-window"; } - // Insights is the digest's home module; a user who turned it off gets no - // digest surfaces at all, so no morning push either. - if (!(await moduleGate(userId, "insights"))) { - annotate({ action: { name: "daily.briefing_push.module_off" } }); - return "module-off"; - } - // One per user per LOCAL day. Anchored on the `push_attempts` ledger the // senders already write (`ok` on a successful delivery), exactly like the // coach-nudge cap: whichever trigger fired first this morning left an `ok` diff --git a/src/lib/jobs/daily-briefing.ts b/src/lib/jobs/daily-briefing.ts index d91d0097a..5edc1a1d7 100644 --- a/src/lib/jobs/daily-briefing.ts +++ b/src/lib/jobs/daily-briefing.ts @@ -40,7 +40,6 @@ export interface DailyBriefingTickSummary { suppressedFrequency: number; noDigest: number; optedOut: number; - moduleOff: number; noChannel: number; outsideWindow: number; failed: number; @@ -74,7 +73,6 @@ export async function runDailyBriefingTick( suppressedFrequency: 0, noDigest: 0, optedOut: 0, - moduleOff: 0, noChannel: 0, outsideWindow: 0, failed: 0, @@ -123,9 +121,6 @@ export async function runDailyBriefingTick( case "opted-out": summary.optedOut += 1; break; - case "module-off": - summary.moduleOff += 1; - break; case "no-channel": summary.noChannel += 1; break; diff --git a/src/lib/jobs/reminder/register-status.ts b/src/lib/jobs/reminder/register-status.ts index 166dc7c14..70ece8c3d 100644 --- a/src/lib/jobs/reminder/register-status.ts +++ b/src/lib/jobs/reminder/register-status.ts @@ -663,7 +663,6 @@ export async function registerStatusQueues( suppressed_frequency: summary.suppressedFrequency, no_digest: summary.noDigest, opted_out: summary.optedOut, - module_off: summary.moduleOff, no_channel: summary.noChannel, outside_window: summary.outsideWindow, failed: summary.failed,