{"record":{"id":"a67c6390f8b60d2d","repo":"koala73/worldmonitor","slug":"failed-to-build-freshness-snapshot","errorCode":null,"errorMessage":"failed to build freshness snapshot","messagePattern":"failed to build freshness snapshot","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"consumer-prices-core/src/api/routes/worldmonitor.ts","lineNumber":67,"sourceCode":"      basket?: string;\n    };\n    try {\n      const data = await buildRetailerSpreadSnapshot(market, basket);\n      return reply.send(data);\n    } catch (err) {\n      fastify.log.error(err);\n      return reply.status(500).send({ error: 'failed to build retailer spread snapshot' });\n    }\n  });\n\n  fastify.get('/freshness', async (request, reply) => {\n    const { market = 'ae' } = request.query as { market?: string };\n    try {\n      const data = await buildFreshnessSnapshot(market);\n      return reply.send(data);\n    } catch (err) {\n      fastify.log.error(err);\n      return reply.status(500).send({ error: 'failed to build freshness snapshot' });\n    }\n  });\n\n  fastify.get('/categories', async (request, reply) => {\n    const { market = 'ae', range = '30d' } = request.query as { market?: string; range?: string };\n    try {\n      const data = await buildCategoriesSnapshot(market, range);\n      return reply.send(data);\n    } catch (err) {\n      fastify.log.error(err);\n      return reply.status(500).send({ error: 'failed to build categories snapshot' });\n    }\n  });\n\n  fastify.get('/basket-series', async (request, reply) => {\n    const { market = 'ae', basket = 'essentials-ae', range = '30d' } = request.query as {\n      market?: string;\n      basket?: string;","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/consumer-prices-core/src/api/routes/worldmonitor.ts#L49-L85","documentation":"The Fastify route GET /worldmonitor/freshness returned 500 because buildFreshnessSnapshot(market) threw. The builder derives per-retailer lastRunAt, status, parse success rate and freshness minutes via the pg pool in src/db/client.ts, so the 500 wraps a database-side throw (missing DATABASE_URL, unreachable Postgres, SQL regression) or a shape expectation on run/parse stats data.","triggerScenarios":"DATABASE_URL unset in the API process; Postgres unreachable or credentials wrong; a migration renaming run-stat columns the freshness query selects; a market with no recorded runs reaching throwing aggregation code.","commonSituations":"Fresh deployment missing the database secret; a market onboarded before any scrape runs have been recorded; local dev against a stopped database.","solutions":["Check the fastify error log for the underlying exception","Verify DATABASE_URL and database reachability from the API host","Confirm the market has at least one recorded retailer run","Reproduce with buildFreshnessSnapshot(market) directly"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"async function freshnessDepsOk(): Promise<boolean> {\n  if (!process.env.DATABASE_URL) return false;\n  try { await query('SELECT 1'); return true; } catch { return false; }\n}","typeGuard":null,"tryCatchPattern":"const res = await fetch('/worldmonitor/freshness?market=ae');\nif (res.status === 500) {\n  await sleep(1_000);\n  return await fetch('/worldmonitor/freshness?market=ae');\n}\nif (!res.ok) throw new Error(`freshness: HTTP ${res.status}`);","preventionTips":["Freshness reads run-stat tables — make sure at least one scrape run is recorded for the market","Cache the last freshness snapshot; it is inherently tolerant of a few minutes of staleness","Correlate 500s here with 500s on other snapshot routes to confirm a database root cause"],"tags":["fastify","http-500","database","snapshot","freshness"],"backgroundTag":"database-query-failed","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-31T04:17:50.494Z"}