{"record":{"id":"7d86d8f889a6b40c","repo":"koala73/worldmonitor","slug":"notice-not-found","errorCode":"NOTICE_NOT_FOUND","errorMessage":"NOTICE_NOT_FOUND","messagePattern":"NOTICE_NOT_FOUND","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/apiPlanLimitNotices.ts","lineNumber":431,"sourceCode":"        .collect();\n      notices.push(...rows.filter((notice) => notice.acknowledgedAt === undefined));\n    }\n    return notices.sort((a, b) => {\n      const severity = (state: ApiPlanLimitNoticeState) =>\n        state === \"over_limit\" ? 3 : state === \"sustained_burst\" ? 2 : 1;\n      const severityDiff = severity(b.state) - severity(a.state);\n      return severityDiff || b.lastSeenAt - a.lastSeenAt;\n    });\n  },\n});\n\nexport const acknowledgeNotice = mutation({\n  args: { noticeId: v.id(\"apiPlanLimitNotices\") },\n  handler: async (ctx, args) => {\n    const userId = await requireUserId(ctx);\n    const notice = await ctx.db.get(args.noticeId);\n    if (!notice || notice.userId !== userId) {\n      throw new ConvexError(\"NOTICE_NOT_FOUND\");\n    }\n    await ctx.db.patch(args.noticeId, { acknowledgedAt: Date.now() });\n    return { ok: true };\n  },\n});\n\nexport const listEmailDue = internalQuery({\n  args: {\n    now: v.number(),\n    limit: v.optional(v.number()),\n  },\n  handler: async (ctx, args) => {\n    const max = args.limit ?? 100;\n    // Scope to `current` in the INDEX so a backlog of superseded (current:false)\n    // pending/failed rows -- which sort first by oldest lastSeenAt -- can never\n    // consume the take() budget and starve genuinely-due live notices.\n    const pending = await ctx.db\n      .query(\"apiPlanLimitNotices\")","sourceCodeStart":413,"sourceCodeEnd":449,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/apiPlanLimitNotices.ts#L413-L449","documentation":"Thrown by acknowledgeNotice when the requested apiPlanLimitNotices row does not exist or exists but belongs to a different userId. As with revokeApiKey, missing and foreign-owned are collapsed into one error to avoid leaking notice ids across users. This guards the acknowledge mutation so a user can only acknowledge their own plan-limit notices.","triggerScenarios":"Calling acknowledgeNotice with a noticeId that was deleted, never existed, or belongs to another user; passing a stale noticeId from an outdated UI list; passing a malformed/truncated Convex id.","commonSituations":"The notice was superseded (current:false) and pruned between the list render and the click; a stale UI rendered an old id; concurrent acknowledgment from another device already mutated state; cross-user id leak.","solutions":["Refresh the notices list and acknowledge a current, owned noticeId.","Treat NOTICE_NOT_FOUND as a no-op success if the notice is already gone/acknowledged.","Validate the noticeId is a valid Convex id for apiPlanLimitNotices."],"exampleFix":"// before\nawait acknowledgeNotice(ctx, { noticeId: staleId }); // NOTICE_NOT_FOUND\n// after — refresh and guard\nconst notices = await listNotices(ctx, {});\nconst target = notices.find(n => n.id === requestedId && !n.acknowledgedAt);\nif (!target) return { ok: true }; // nothing to acknowledge\nawait acknowledgeNotice(ctx, { noticeId: target.id });","handlingStrategy":"validation","validationCode":"const notices = await listNotices(ctx, {});\nconst target = notices.find(n => n.id === noticeId && !n.acknowledgedAt);\nif (!target) return { ok: true };\nawait acknowledgeNotice(ctx, { noticeId: target.id });","typeGuard":null,"tryCatchPattern":"try {\n  await acknowledgeNotice(ctx, { noticeId });\n} catch (e) {\n  if (e instanceof ConvexError && e.message === \"NOTICE_NOT_FOUND\") {\n    // notice gone/superseded — nothing to acknowledge\n  } else throw e;\n}","preventionTips":["Refresh notices before acknowledging.","Treat NOTICE_NOT_FOUND as success in idempotent acknowledge flows.","Filter out acknowledged notices from the UI list."],"tags":["convex","notices","not-found","tenancy","ownership"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}