{"record":{"id":"87240ae15d89706b","repo":"koala73/worldmonitor","slug":"redis-down","errorCode":"REDIS_DOWN","errorMessage":"Redis not configured","messagePattern":"Redis not configured","errorType":"http","errorClass":null,"httpStatus":503,"severity":"warning","filePath":"api/health.js","lineNumber":3597,"sourceCode":"// problem, so adding a new ok-bucket status (NOT_CONFIGURED) can never again\n// leave one problem surface silently reporting it. An unregistered status is\n// treated as a problem, matching the summary's `STATUS_COUNTS[s] ?? 'warn'`.\nfunction isProblemStatus(status) {\n  return STATUS_COUNTS[status] !== 'ok';\n}\n\nfunction isPendingHealthEntry(entry, now) {\n  return isProblemStatus(entry?.status) && healthStatusBucket(entry, now) === 'ok';\n}\n\n/**\n * True when a cached verdict still carries a health softening whose published\n * deadline has passed. Reads both snapshot shapes:\n * the full one keyed by `checks`, the compact one by `problems` and `pending`.\n */\nfunction isExpiredDeadline(raw, now) {\n  const until = Date.parse(typeof raw === 'string' ? raw : '');\n  // An unparseable deadline is treated as expired: a pending entry that\n  // cannot prove it is still inside its window must not be served warm.\n  return !Number.isFinite(until) || now >= until;\n}\n\n// Every per-entry softening deadline a snapshot can carry, in one place. Both\n// readers below walk this table instead of repeating a `hasOwnProperty` +\n// `isExpiredDeadline` block per field, so adding another softening is one\n// entry here rather than more near-identical branches that can drift apart.\n//\n// `kind` decides which `hasExpiredActivationGrace` toggle governs the field.\n// `status` pins a field to one status where the field alone is not proof:\n// `rolloutPendingUntil` only softens a ROLLOUT_PENDING entry, whereas the two\n// content deadlines are emitted precisely when they apply.\nconst ENTRY_SOFTENING_DEADLINES = [\n  { field: 'rolloutPendingUntil', kind: 'rollout', status: 'ROLLOUT_PENDING' },\n  { field: 'contentFreshnessPendingUntil', kind: 'content', status: null },\n  { field: 'staleContentGraceUntil', kind: 'content', status: null },\n  { field: 'sourceFailurePendingUntil', kind: 'source', status: 'SEED_ERROR' },","sourceCodeStart":3579,"sourceCodeEnd":3615,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/api/health.js#L3579-L3615","documentation":"listApiKeys() treats a null Clerk user or null Convex client as an empty list (no throw). It throws only when waitForConvexAuthForUser(userId) is false while assertAccountStillCurrent(userId) passes, i.e., the user is still signed in as the same person but the Convex auth token never became ready within the 10s barrier timeout. This distinguishes a transient token-propagation failure from a real account switch, so the UI can show 'unknown' instead of a false 'no keys'.","triggerScenarios":"Clerk session valid but the Convex auth rebind (startConvexAuthRebind/installConvexAuth) never delivers a server-confirmed token within 10s; slow or blocked token fetch; WebSocket pause preventing the barrier from completing.","commonSituations":"Slow networks on first load; ad-blockers blocking Clerk or Convex endpoints; a backgrounded tab with throttled timers; a brief Convex deployment hiccup.","solutions":["Retry the load after a short delay; token propagation usually completes on the next attempt","Verify network access to both Clerk and the Convex deployment (no blocked requests in the Network tab)","Reload the page to force a fresh auth rebind if it persists","Render the key list as 'unavailable, retry' rather than empty when this error fires"],"exampleFix":"// before\nconst keys = await listApiKeys(); // can throw 'Authentication unavailable while loading API keys.'\n\n// after: one bounded retry, then an explicit unavailable state\nlet keys: ApiKeyInfo[] | null = null;\nfor (let attempt = 0; attempt < 2 && !keys; attempt++) {\n  try { keys = await listApiKeys(); }\n  catch (e) {\n    if (attempt === 1 || !(e instanceof Error && e.message.includes('Authentication unavailable'))) throw e;\n    await new Promise(r => setTimeout(r, 1500));\n  }\n}\nrenderKeys(keys ?? 'unavailable');","handlingStrategy":"retry","validationCode":"const userId = getCurrentClerkUser()?.id;\nif (!userId) { renderKeys([]); return; } // signed out is genuinely empty\nif (!(await getConvexClient())) { renderKeys([]); return; }","typeGuard":"const isAuthUnavailableError = (e: unknown): e is Error =>\n  e instanceof Error && e.message.startsWith('Authentication unavailable');","tryCatchPattern":"try {\n  const keys = await listApiKeys();\n  renderKeys(keys);\n} catch (e) {\n  if (isAuthUnavailableError(e)) renderKeys('unavailable'); // NOT empty — truth is unknown\n  else throw e;\n}","preventionTips":["Never render this failure as an empty list; show an explicit 'unavailable, retry' state","One bounded retry after 1-2s covers most slow token propagation","Check for blocked Clerk/Convex requests when it recurs"],"tags":["clerk","convex","auth-token","timeout","api-keys"],"backgroundTag":"auth-token-not-ready","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}