fix(admin): wrap post-auth data loads in try/catch to surface and survive SC render errors
Deploy to route.crispygoat.com / deploy (push) Successful in 3m9s

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.
This commit is contained in:
2026-06-06 22:35:24 +00:00
parent 5654ebaecd
commit 3f731f7739
2 changed files with 31 additions and 14 deletions
+18 -6
View File
@@ -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<ReturnType<typeof listBrandsForAdmin>> = [];
try {
brands = await listBrandsForAdmin();
} catch (err) {
console.error("[admin/layout] listBrandsForAdmin failed:", err);
}
return (
<ToastProviderWrapper>
+6 -1
View File
@@ -27,9 +27,11 @@ 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") {
try {
const { data: firstBrand } = await supabase
.from("brands")
.select("id")
@@ -38,6 +40,9 @@ export default async function AdminPage() {
if (firstBrand?.id) {
dashboardBrandId = firstBrand.id;
}
} catch (err) {
console.error("[admin/page] supabase brands lookup failed:", err);
}
}
if (dashboardBrandId) {