diff --git a/MEMORY.md b/MEMORY.md index 917ecd2..fd10a89 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -286,3 +286,25 @@ Follow-up pass on the original Codex review covering public site, buyer path, bi ### Migration 203 — applied via Supabase CLI `203_plan_usage_active_products.sql` updates `get_brand_plan_info` to count `products` where `active = true AND deleted_at IS NULL`, matching the dashboard's "Active Products" stat. The `NOTIFY pgrst, 'reload schema'` ensures PostgREST picks up the change without restart. + +## Gitea build fix — 2026-06-06 + +Gitea runner (`https://git.crispygoat.com/tyler/route-commerce.git`, branch `main`) was failing `next build` with two errors: + +1. **DYNAMIC_SERVER_USAGE** on `/admin/settings/square-sync` (and the whole admin tree): `getAdminUser()` reads `cookies()` via `next/headers`. The admin layout tried to prerender statically, so the first child page that hit cookies aborted the build. +2. **Prerender ECONNREFUSED** on `/indian-river-direct/stops`: `getPublicStopsForBrand` / `getActiveStopsForSitemap` / `getBrandSettingsPublic` fetch `NEXT_PUBLIC_SUPABASE_URL` at build time. The Gitea runner passes a Supabase URL that resolves but is unreachable, so `fetch` throws `ECONNREFUSED` and the prerender aborts. + +The earlier commit `2f3be54 fix(actions): skip Supabase fetch at build time when env vars unset` only added `if (!supabaseUrl || !supabaseKey) return [];` — but in CI the env vars **are** set, so the guard passed and the fetch was still attempted. + +### Fixes applied + +- `src/actions/stops.ts` — wrapped `getActiveStopsForSitemap` and `getPublicStopsForBrand` fetches in `try/catch` returning `[]` on error. Env-var guard kept as fast path. +- `src/actions/brand-settings.ts` — wrapped `getBrandSettingsPublic` fetch in `try/catch` returning `{ success: false }` on error. +- `src/app/admin/settings/square-sync/page.tsx` — added `export const dynamic = "force-dynamic";` (was missing). +- `src/app/admin/layout.tsx` — added `export const dynamic = "force-dynamic";` so the entire admin tree opts out of static prerender (layout calls `getAdminUser()` which reads cookies). +- `.gitea/workflows/deploy.yml` was simplified earlier in commit `2d837bc` to a thin wrapper around `deploy/deploy.sh`. + +### Remote + +- The crispygoat repo (`git@git.crispygoat.com:tyler/route-commerce.git`) and the GitHub `origin` repo are separate forks — `tyler/main` is the self-hosted Auth.js + Postgres branch, `origin/main` is the Supabase branch. Don't merge them; they share no deploy workflow. +- Push targets `tyler/main` to trigger the Gitea build. diff --git a/src/actions/brand-settings.ts b/src/actions/brand-settings.ts index 8182617..dbd4dfb 100644 --- a/src/actions/brand-settings.ts +++ b/src/actions/brand-settings.ts @@ -279,22 +279,29 @@ export async function getBrandSettingsPublic(brandSlug: string): Promise { // an empty list lets the sitemap render with only the static URLs. if (!supabaseUrl || !supabaseKey) return []; - // Get all active stops with their brand slug - const response = await fetch( - `${supabaseUrl}/rest/v1/rpc/get_active_stops_with_brand`, - { - method: "POST", - headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" }, - } - ); + // Get all active stops with their brand slug. Wrapped in try/catch so a + // build-time outage (ECONNREFUSED) doesn't crash the prerender — the + // sitemap just renders without stop URLs. + try { + const response = await fetch( + `${supabaseUrl}/rest/v1/rpc/get_active_stops_with_brand`, + { + method: "POST", + headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" }, + } + ); - if (!response.ok) return []; + if (!response.ok) return []; - const stops = await response.json(); - return Array.isArray(stops) ? stops : []; + const stops = await response.json(); + return Array.isArray(stops) ? stops : []; + } catch { + return []; + } } /** @@ -165,21 +171,28 @@ export async function getPublicStopsForBrand( // fetch path is unchanged. if (!supabaseUrl || !supabaseKey) return []; - const response = await fetch( - `${supabaseUrl}/rest/v1/rpc/get_public_stops_for_brand`, - { - method: "POST", - headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" }, - body: JSON.stringify({ p_brand_slug: brandSlug }), - next: { - revalidate: 300, - tags: ["stops", `brand:${brandSlug}:stops`], - }, - } - ); + // Wrapped in try/catch so a build-time Supabase outage (ECONNREFUSED) + // doesn't crash the prerender — the page just renders with no stops + // and revalidates from a real request once the cache is warm. + try { + const response = await fetch( + `${supabaseUrl}/rest/v1/rpc/get_public_stops_for_brand`, + { + method: "POST", + headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" }, + body: JSON.stringify({ p_brand_slug: brandSlug }), + next: { + revalidate: 300, + tags: ["stops", `brand:${brandSlug}:stops`], + }, + } + ); - if (!response.ok) return []; + if (!response.ok) return []; - const stops = await response.json(); - return Array.isArray(stops) ? (stops as PublicStop[]) : []; + const stops = await response.json(); + return Array.isArray(stops) ? (stops as PublicStop[]) : []; + } catch { + return []; + } } \ No newline at end of file diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 3277909..c275666 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -8,6 +8,11 @@ import "@/styles/admin-design-system.css"; import { ToastProvider } from "@/components/admin/Toast"; import { ToastContainer } from "@/components/admin/ToastContainer"; +// Admin layout calls getAdminUser() which reads cookies(). Without this, +// Next.js tries to prerender the entire /admin/* tree statically and the +// first page that hits cookies() aborts the build with DYNAMIC_SERVER_USAGE. +export const dynamic = "force-dynamic"; + // Toast provider wrapper component function ToastProviderWrapper({ children }: { children: React.ReactNode }) { return ( diff --git a/src/app/admin/settings/square-sync/page.tsx b/src/app/admin/settings/square-sync/page.tsx index 032dd80..167ace1 100644 --- a/src/app/admin/settings/square-sync/page.tsx +++ b/src/app/admin/settings/square-sync/page.tsx @@ -4,6 +4,10 @@ import { getPaymentSettings } from "@/actions/payments"; import { getSyncLog, type SyncLogEntry } from "@/actions/square-sync-ui"; import SquareSyncSettingsClient from "./SquareSyncSettingsClient"; +// Uses cookies() via getAdminUser — must be dynamic to avoid the +// "couldn't be rendered statically" build error. +export const dynamic = "force-dynamic"; + export default async function SquareSyncSettingsPage() { const adminUser = await getAdminUser(); if (!adminUser) redirect("/login");