diff --git a/CHANGELOG.md b/CHANGELOG.md index e07d420..3da3e1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together. +## 0.55.2 — 2026-07-19 18:20 + +### Fixed +- Admin Insights page could 500 the whole panel if any one of its six aggregate queries failed — replaced a non-standard positional GROUP BY with the expression-repeat pattern used everywhere else in the codebase, and made the six queries independent (Promise.allSettled) so one failing degrades that one chart instead of crashing the page. + ## 0.55.1 — 2026-07-19 18:05 ### Fixed diff --git a/apps/web/app/admin/insights/page.tsx b/apps/web/app/admin/insights/page.tsx index 2020ae8..b1aaddc 100644 --- a/apps/web/app/admin/insights/page.tsx +++ b/apps/web/app/admin/insights/page.tsx @@ -43,28 +43,24 @@ export default async function AdminInsightsPage() { const since = new Date(); since.setDate(since.getDate() - DAYS); - const [ - signupRows, - recipeRows, - tierRows, - visibilityRows, - usageRows, - ticketRows, - ] = await Promise.all([ + // Promise.allSettled, not all — six independent aggregate queries feeding + // six independent charts; one query breaking (e.g. a table that's empty + // in a fresh install) shouldn't take down every chart on the page. + const results = await Promise.allSettled([ db - .select({ day: sql`to_char(${users.createdAt}, 'YYYY-MM-DD')`, n: sql`count(*)::int` }) + .select({ day: sql`to_char(${users.createdAt}, 'YYYY-MM-DD')`.as("day"), n: sql`count(*)::int` }) .from(users) .where(gte(users.createdAt, since)) - .groupBy(sql`1`), + .groupBy(sql`to_char(${users.createdAt}, 'YYYY-MM-DD')`), db .select({ - day: sql`to_char(${recipes.createdAt}, 'YYYY-MM-DD')`, + day: sql`to_char(${recipes.createdAt}, 'YYYY-MM-DD')`.as("day"), aiGenerated: recipes.aiGenerated, n: sql`count(*)::int`, }) .from(recipes) .where(gte(recipes.createdAt, since)) - .groupBy(sql`1`, recipes.aiGenerated), + .groupBy(sql`to_char(${recipes.createdAt}, 'YYYY-MM-DD')`, recipes.aiGenerated), db.select({ tier: users.tier, n: sql`count(*)::int` }).from(users).groupBy(users.tier), db.select({ visibility: recipes.visibility, n: sql`count(*)::int` }).from(recipes).groupBy(recipes.visibility), db @@ -74,6 +70,21 @@ export default async function AdminInsightsPage() { db.select({ status: supportTickets.status, n: sql`count(*)::int` }).from(supportTickets).groupBy(supportTickets.status), ]); + for (const r of results) { + if (r.status === "rejected") console.error("[admin/insights] query failed", r.reason); + } + + const [signupRows, recipeRows, tierRows, visibilityRows, usageRows, ticketRows] = results.map((r) => + r.status === "fulfilled" ? r.value : [] + ) as [ + { day: string; n: number }[], + { day: string; aiGenerated: boolean; n: number }[], + { tier: "free" | "pro" | "family"; n: number }[], + { visibility: "private" | "unlisted" | "public" | "followers"; n: number }[], + { month: string; n: number }[], + { status: "open" | "triaged" | "closed"; n: number }[], + ]; + const signupByDay = new Map(signupRows.map((r) => [r.day, r.n])); const signupSeries = lastNDays(DAYS).map((day) => ({ date: day, value: signupByDay.get(day) ?? 0 })); diff --git a/apps/web/lib/changelog.ts b/apps/web/lib/changelog.ts index 168f1ca..e389612 100644 --- a/apps/web/lib/changelog.ts +++ b/apps/web/lib/changelog.ts @@ -1,5 +1,5 @@ // Mirrors CHANGELOG.md at the repo root — update both together. -export const APP_VERSION = "0.55.1"; +export const APP_VERSION = "0.55.2"; export type ChangelogEntry = { version: string; @@ -11,6 +11,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + version: "0.55.2", + date: "2026-07-19 18:20", + fixed: [ + "Admin Insights page could 500 the whole panel if any one of its six aggregate queries failed — replaced a non-standard positional GROUP BY with the same expression-repeat pattern used everywhere else in the codebase, and made the six queries independent (Promise.allSettled) so one failing degrades that one chart instead of crashing the page.", + ], + }, { version: "0.55.1", date: "2026-07-19 18:05", diff --git a/apps/web/package.json b/apps/web/package.json index deaf9c4..f085295 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@epicure/web", - "version": "0.55.1", + "version": "0.55.2", "private": true, "scripts": { "dev": "next dev", diff --git a/package.json b/package.json index 51fccd8..fbe58df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "epicure", - "version": "0.55.1", + "version": "0.55.2", "private": true, "scripts": { "dev": "pnpm --filter web dev",