From 6eb759dd431430f02cdf41bd0d22c93bc4d0dfb1 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sun, 19 Jul 2026 20:43:11 +0200 Subject: [PATCH] fix: Admin Insights page could crash on a single query failure (v0.55.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes: 1. Replaced positional `GROUP BY 1` (not used anywhere else in this codebase) with the conventional pattern of repeating the actual grouped expression — matches every other groupBy call site in the app. 2. Switched Promise.all -> Promise.allSettled across the six independent aggregate queries feeding the six charts, defaulting a failed one to an empty array (that chart just renders "No data yet") instead of taking the whole page down. Logs the failure server-side either way. Couldn't reproduce locally (no DB in this sandbox), but confirmed the allSettled path works: the production build itself hit a real connection failure against the (absent) local DB and degraded cleanly instead of crashing, which is exactly the resilience this fixes. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 5 ++++ apps/web/app/admin/insights/page.tsx | 35 ++++++++++++++++++---------- apps/web/lib/changelog.ts | 9 ++++++- apps/web/package.json | 2 +- package.json | 2 +- 5 files changed, 38 insertions(+), 15 deletions(-) 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",