{"record":{"id":"af4698d293a2e115","repo":"koala73/worldmonitor","slug":"digesthour-must-be-an-integer-0-23","errorCode":null,"errorMessage":"digestHour must be an integer 0–23","messagePattern":"digestHour must be an integer 0–23","errorType":"validation","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/alertRules.ts","lineNumber":287,"sourceCode":"    }\n  },\n});\n\nexport const setDigestSettings = mutation({\n  args: {\n    variant: v.string(),\n    digestMode: digestModeValidator,\n    digestHour: v.optional(v.number()),\n    digestTimezone: v.optional(v.string()),\n  },\n  handler: async (ctx, args) => {\n    const identity = await ctx.auth.getUserIdentity();\n    if (!identity) throw new ConvexError(\"UNAUTHENTICATED\");\n    const userId = identity.subject;\n    await assertProEntitlement(ctx, userId);\n\n    if (args.digestHour !== undefined && (args.digestHour < 0 || args.digestHour > 23 || !Number.isInteger(args.digestHour))) {\n      throw new ConvexError(\"digestHour must be an integer 0–23\");\n    }\n    if (args.digestTimezone !== undefined) {\n      try {\n        Intl.DateTimeFormat(undefined, { timeZone: args.digestTimezone });\n      } catch {\n        throw new ConvexError(\"digestTimezone must be a valid IANA timezone (e.g. America/New_York)\");\n      }\n    }\n\n    const existing = await ctx.db\n      .query(\"alertRules\")\n      .withIndex(\"by_user_variant\", (q) =>\n        q.eq(\"userId\", userId).eq(\"variant\", args.variant),\n      )\n      .unique();\n\n    const pair = resolveEffectivePair({\n      incomingDigestMode: args.digestMode,","sourceCodeStart":269,"sourceCodeEnd":305,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/alertRules.ts#L269-L305","documentation":"Thrown by setDigestSettings when args.digestHour is present but outside [0,23], or is not an integer. digestHour represents the hour-of-day at which a digest is delivered and must be a whole number 0–23. The validation runs after the auth/entitlement gate and before any DB read.","triggerScenarios":"Calling setDigestSettings with digestHour = 24, -1, 23.5, NaN, or any non-integer numeric value.","commonSituations":"Client passing a 1-based hour (1–24) instead of 0-based; a time picker returning minutes or fractional hours; a timezone offset calculation producing an out-of-range value; JSON deserialization of a float where an int was expected.","solutions":["Coerce and range-check digestHour to an integer in [0,23] before submitting (use Math.floor and clamp/mod).","Ensure the UI time picker emits 0-based hours (00:00–23:59).","Omit digestHour entirely if you do not need to change it — the field is optional."],"exampleFix":"// before\nawait setDigestSettings({ variant, digestMode: 'daily', digestHour: 24 });\n// after\nconst hour = Math.max(0, Math.min(23, Math.floor(pickerHour)) % 24);\nawait setDigestSettings({ variant, digestMode: 'daily', digestHour: hour });","handlingStrategy":"validation","validationCode":"function isValidDigestHour(h) {\n  return typeof h === 'number' && Number.isInteger(h) && h >= 0 && h <= 23;\n}\nif (args.digestHour !== undefined && !isValidDigestHour(args.digestHour)) {\n  // surface error, do not call mutation\n}","typeGuard":"function isDigestHour(n): n is number {\n  return typeof n === 'number' && Number.isInteger(n) && n >= 0 && n <= 23;\n}","tryCatchPattern":"try {\n  await setDigestSettings(args);\n} catch (e) {\n  if (e instanceof ConvexError && /digestHour must be an integer/.test(String(e.message))) {\n    // coerce: args.digestHour = Math.floor(args.digestHour) % 24 and retry once\n  } else throw e;\n}","preventionTips":["Use a 0-based 24-hour integer picker for digest scheduling.","Coerce with Math.floor and clamp to [0,23] before submit.","Omit digestHour when not changing it."],"tags":["convex","validation","notifications","datetime"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}