From 3f731f7739126a59a3fe3000a45ff0b44b1327fb Mon Sep 17 00:00:00 2001 From: default Date: Sat, 6 Jun 2026 22:35:24 +0000 Subject: [PATCH] fix(admin): wrap post-auth data loads in try/catch to surface and survive SC render errors The /admin page was throwing a Server Components render error (digest 4266906817) in production. The exact throw point was not visible from the browser (Next.js hides it for security) and server logs were not available at debugging time. The layout and page both call data-loading functions (getActiveBrandId, listBrandsForAdmin, supabase brands lookup) that could throw on a transient DB/network failure, and these calls had no try/catch. A single failed call would crash the entire admin shell. Wrap each call in try/catch with console.error so: 1. The page renders with sensible defaults if a call fails 2. The actual error is logged server-side (visible via the digest in the admin error boundary or PM2/Docker logs) 3. The admin shell stays functional even if a single data source is down No behavior change on the happy path. --- src/app/admin/layout.tsx | 24 ++++++++++++++++++------ src/app/admin/page.tsx | 21 +++++++++++++-------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index c275666..0d42720 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -64,13 +64,25 @@ export default async function AdminLayout({ children }: { children: React.ReactN redirect("/change-password"); } - // Resolve the active brand (URL > cookie > legacy > first of brand_ids) - const activeBrandId = await getActiveBrandId(adminUser); + // Resolve the active brand (URL > cookie > legacy > first of brand_ids). + // Wrapped in try/catch so a transient brand-resolution failure can't + // crash the whole admin shell — we fall back to null (no active brand). + let activeBrandId: string | null = null; + try { + activeBrandId = await getActiveBrandId(adminUser); + } catch (err) { + console.error("[admin/layout] getActiveBrandId failed:", err); + } - // Fetch accessible brands for the sidebar BrandSelector. We do this - // unconditionally — `listBrandsForAdmin` is cheap and the sidebar - // decides whether to show the dropdown. - const brands = await listBrandsForAdmin(); + // Fetch accessible brands for the sidebar BrandSelector. Wrapped in + // try/catch so the sidebar renders empty rather than crashing the page + // if the brands query fails. + let brands: Awaited> = []; + try { + brands = await listBrandsForAdmin(); + } catch (err) { + console.error("[admin/layout] listBrandsForAdmin failed:", err); + } return ( diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 30f4fc3..a335a66 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -27,16 +27,21 @@ export default async function AdminPage() { // Resolve active brand via the canonical resolver (URL > cookie > legacy brand_id // > first of brand_ids). For platform_admin in dev mode this is null, so we // fall back to the first brand in the brands table to keep the dashboard's - // "Active Products" stat in sync with the billing page. + // "Active Products" stat in sync with the billing page. Wrapped in try/catch + // so a transient DB/network failure can't crash the whole admin page. let dashboardBrandId: string | null = adminUser ? await getActiveBrandId(adminUser) : null; if (!dashboardBrandId && adminUser?.role === "platform_admin") { - const { data: firstBrand } = await supabase - .from("brands") - .select("id") - .limit(1) - .single(); - if (firstBrand?.id) { - dashboardBrandId = firstBrand.id; + try { + const { data: firstBrand } = await supabase + .from("brands") + .select("id") + .limit(1) + .single(); + if (firstBrand?.id) { + dashboardBrandId = firstBrand.id; + } + } catch (err) { + console.error("[admin/page] supabase brands lookup failed:", err); } }