fix(frontend): resolve app-wide 'Oops' crash + PWA middleware interception
Deploy to Production / test (push) Has been cancelled
Deploy to Production / deploy (push) Has been cancelled

Root cause (found via Playwright browser probe — curl could not detect client-side
hydration errors):

1. ROOT-LAYOUT INTL CRASH (the 'Oops' on every page incl /login):
   app/layout.tsx renders global client components (PwaInstallPrompt → useTranslations,
   Toaster, Sonner) as siblings of {children} inside <Providers>, but only each
   route-group layout wrapped its own children in NextIntlClientProvider. So those
   global components mounted with NO intl context → 'No intl context found' → React
   hydration crash → global-error 'Oops'. Fix: wrap the root body in
   NextIntlClientProvider via getMessages() (RootLayout now async). Nested providers
   stay valid (next-intl supports nesting).

2. PWA MIDDLEWARE INTERCEPTION (manifest.json syntax error + stale cache):
   middleware matcher did not exclude /manifest.json or /sw.js, so unauthenticated
   browsers got 307→/login (HTML) for both. Browser parsed HTML as JSON
   ('manifest.json:1 Syntax error') and an HTML/old service worker kept serving
   stale bundles ('website hasn't changed' after redeploys). Fix: exclude
   manifest.json, sw.js, icons, offline from the matcher.

3. SERVICE-WORKER STALE CACHE: bump CACHE_NAME v1→v2 so the activate handler purges
   old cached bundles from clients that loaded the broken build.

Also adds scripts/debug/dashboard-probe.mjs — a Playwright probe that logs in and
captures real client-side console/network errors + screenshot.
This commit is contained in:
Patrick Plate
2026-06-13 10:36:09 +02:00
parent 2347a7a1d9
commit 4be9c4cf2c
6 changed files with 105 additions and 11 deletions
+5 -1
View File
@@ -1,6 +1,10 @@
/// <reference lib="webworker" />
const CACHE_NAME = "cannamanage-v1"
// Bump this version on every release that changes cached assets. The `activate`
// handler below deletes all caches whose name !== CACHE_NAME, so incrementing
// this string force-purges stale bundles from clients that cached the old
// (broken) build — fixes "website hasn't changed after redeploy".
const CACHE_NAME = "cannamanage-v2"
const OFFLINE_URL = "/offline"
// Assets to pre-cache
+20 -8
View File
@@ -1,4 +1,6 @@
import { Cairo, Lato } from "next/font/google"
import { NextIntlClientProvider } from "next-intl"
import { getMessages } from "next-intl/server"
import { cn } from "@/lib/utils"
@@ -50,9 +52,17 @@ const cairoFont = Cairo({
variable: "--font-cairo",
})
export default function RootLayout(props: { children: ReactNode }) {
export default async function RootLayout(props: { children: ReactNode }) {
const { children } = props
// Load messages at the root so GLOBAL components rendered here (PwaInstallPrompt,
// Toaster, etc.) have next-intl context. Without this, those components — which
// are siblings of {children} and therefore outside every route-group's
// NextIntlClientProvider — call useTranslations() with no provider and crash
// hydration on EVERY page (the "Oops" error). Nested route-group providers
// remain valid; next-intl supports provider nesting.
const messages = await getMessages()
return (
<html lang="en" dir="ltr" suppressHydrationWarning>
<body
@@ -63,13 +73,15 @@ export default function RootLayout(props: { children: ReactNode }) {
cairoFont.variable // Include Cairo font variable
)}
>
<Providers locale="de">
{children}
<Toaster />
<Sonner />
<SwRegistration />
<PwaInstallPrompt />
</Providers>
<NextIntlClientProvider messages={messages}>
<Providers locale="de">
{children}
<Toaster />
<Sonner />
<SwRegistration />
<PwaInstallPrompt />
</Providers>
</NextIntlClientProvider>
</body>
</html>
)
+7 -2
View File
@@ -61,8 +61,13 @@ export const config = {
// - /portal-login (portal auth page)
// - /api/auth (NextAuth API routes)
// - /_next/static, /_next/image (Next.js internals)
// - /favicon.ico, /images (public assets)
// - /favicon.ico, /images, /icons (public assets)
// - /manifest.json, /sw.js, /offline (PWA assets — MUST be public, otherwise the
// browser fetches them unauthenticated, gets a 307→/login HTML page, and:
// (1) parses the HTML as JSON → "manifest.json:1 Syntax error",
// (2) registers an HTML "sw.js" or keeps a STALE service worker in control →
// cached old bundles keep serving → "website hasn't changed" after redeploys.
// - /pricing, /impressum, /datenschutz, /agb (public marketing pages)
"/((?!login|register|forgot-password|portal-login|api/auth|_next/static|_next/image|favicon.ico|images|pricing|impressum|datenschutz|agb).*)",
"/((?!login|register|forgot-password|portal-login|api/auth|_next/static|_next/image|favicon.ico|images|icons|manifest.json|sw.js|offline|pricing|impressum|datenschutz|agb).*)",
],
}