{"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/eeab0a219fce0f02a00603b532dbae9041b934ac/convex/alertRules.ts#L269-L305","documentation":"setDigestSettings validates the optional digestHour argument when supplied: it must satisfy Number.isInteger and the range 0–23 (convex/alertRules.ts:286). Fractional hours (7.5), negative values, 24+, or NaN throw a plain ConvexError with string data 'digestHour must be an integer 0–23' (no structured code). The arg itself is v.optional(v.number()), so non-numeric types are rejected earlier by the Convex validator with a different error.","triggerScenarios":"setDigestSettings with digestHour: 7.5 (a time picker emitting fractional hours); digestHour: 24 (a '24:00' convention); a computed hour like utcHour + tzOffset overflowing past 23 without wrapping; parseFloat/parseInt producing NaN or a float from free-text input.","commonSituations":"Digest-time pickers built from timezone conversions that can exceed the range (e.g. UTC 22 + offset 3 = 25); importing cron-style hours where 24 means midnight; UI sliders emitting floats; copy-paste of '8:30' parsed as 8.5.","solutions":["Pass an integer 0–23, or omit digestHour entirely — it is optional.","Wrap computed hours into range: ((hour % 24) + 24) % 24, and round or truncate fractional picker output.","Map imported '24:00' values to 0 before sending.","Run the same integer/range check client-side before calling (see defense) so the user gets inline feedback instead of a mutation error."],"exampleFix":"// before\nawait mutateAPI.alertRules.setDigestSettings({ variant: 'default', digestMode: 'daily', digestHour: 24 });\n// ConvexError: digestHour must be an integer 0–23\n\n// after — wrap into range and force integer\nconst digestHour = ((Math.trunc(selectedHour) % 24) + 24) % 24;\nawait mutateAPI.alertRules.setDigestSettings({ variant: 'default', digestMode: 'daily', digestHour });","handlingStrategy":"validation","validationCode":"// Same predicate the server applies, run before the mutation.\nconst isValidHour = (h: unknown): h is number =>\n  typeof h === 'number' && Number.isInteger(h) && h >= 0 && h <= 23;\n\nconst digestHour = computedHour === undefined ? undefined : ((Math.trunc(computedHour) % 24) + 24) % 24;\nif (digestHour !== undefined && !isValidHour(digestHour)) {\n  throw new RangeError('digestHour must be an integer 0-23');\n}\nawait mutateAPI.alertRules.setDigestSettings({ variant, digestMode, digestHour });","typeGuard":"const isValidDigestHour = (h: unknown): h is number =>\n  typeof h === 'number' && Number.isInteger(h) && h >= 0 && h <= 23;","tryCatchPattern":"try {\n  await mutateAPI.alertRules.setDigestSettings(args);\n} catch (err) {\n  if (err instanceof ConvexError && err.data === 'digestHour must be an integer 0–23') {\n    setFieldError('digestHour', 'Pick a whole hour between 0 and 23');\n    return;\n  }\n  throw err;\n}","preventionTips":["Build hour inputs from a 0–23 list so invalid values cannot be selected.","Wrap timezone-adjusted hours with ((h % 24) + 24) % 24 and truncate floats.","Map imported '24:00' to 0 before sending.","Run the integer/range predicate client-side for inline feedback instead of a mutation round-trip."],"tags":["convex","validation","arguments","range","digest"],"backgroundTag":"number-out-of-range","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}