diff --git a/package.json b/package.json index 293c74a1..835e821e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "packageManager": "pnpm@10.33.2", "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev --turbopack -H 0.0.0.0", "build": "next build && pnpm build:server", "build:server": "esbuild src/server.ts --bundle --platform=node --target=node20 --format=cjs --outdir=dist --packages=external --tsconfig=tsconfig.server.json", "build:worker": "esbuild src/worker.ts --bundle --platform=node --target=node20 --format=cjs --outdir=dist --packages=external --tsconfig=tsconfig.server.json", diff --git a/src/app/api/v1/website-analytics/route.ts b/src/app/api/v1/website-analytics/route.ts index cdef0afe..1ee28b60 100644 --- a/src/app/api/v1/website-analytics/route.ts +++ b/src/app/api/v1/website-analytics/route.ts @@ -104,9 +104,14 @@ export const GET = withAuth( } // `data === null` from the service means Umami isn't configured for - // this port - surface that explicitly so the UI can render a - // "configure your credentials" empty state instead of a chart. - if (data === null) throw new CodedError('UMAMI_NOT_CONFIGURED'); + // this port. Return 200 with `data: null` + the explicit + // `notConfigured: true` flag so the UI renders a "configure your + // credentials" empty state. **Do not throw** — a 409 here would + // trigger React Query's default retry loop, and every retry fires + // a system_settings lookup → pool saturation → server hang. + if (data === null) { + return NextResponse.json({ metric, range, data: null, notConfigured: true }); + } return NextResponse.json({ metric, range, data }); } catch (err) { diff --git a/src/components/dashboard/website-glance-tile.tsx b/src/components/dashboard/website-glance-tile.tsx index 8326b130..893a9115 100644 --- a/src/components/dashboard/website-glance-tile.tsx +++ b/src/components/dashboard/website-glance-tile.tsx @@ -27,10 +27,9 @@ export function WebsiteGlanceTile() { // Hide the tile entirely if Umami isn't configured - this dashboard is // for sales, not for prompting the operator into integration setup. - if ( - stats.data?.error === 'umami_not_configured' || - active.data?.error === 'umami_not_configured' - ) { + // The API surfaces `notConfigured: true` on a 200 response so React + // Query doesn't retry-loop (a prior 409-throw caused server hangs). + if (stats.data?.notConfigured || active.data?.notConfigured) { return null; } diff --git a/src/components/shared/branded-auth-shell.tsx b/src/components/shared/branded-auth-shell.tsx index 83177efc..fc26e3c2 100644 --- a/src/components/shared/branded-auth-shell.tsx +++ b/src/components/shared/branded-auth-shell.tsx @@ -30,11 +30,17 @@ interface BrandedAuthShellProps { export function BrandedAuthShell({ children, branding }: BrandedAuthShellProps) { const logoUrl = branding?.logoUrl || DEFAULT_LOGO_URL; const altText = branding?.appName || 'Port Nimara'; + // fixed inset-0 anchors the auth surface to the viewport directly — + // iOS Safari ignores overflow-hidden on inner divs for body-level + // scrolling, so a regular `h-[100dvh] overflow-hidden` wrapper doesn't + // stop the rubber-band bounce. Pinning to the viewport via position + // fixed does. The fixed-position shell then uses flex to center the + // card within the visible area. return ( -