From 4be9c4cf2c7fb10c50fff00f3c036659fb6ac1a1 Mon Sep 17 00:00:00 2001 From: Patrick Plate Date: Sat, 13 Jun 2026 10:36:09 +0200 Subject: [PATCH] fix(frontend): resolve app-wide 'Oops' crash + PWA middleware interception MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 , 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. --- cannamanage-frontend/public/sw.js | 6 ++- cannamanage-frontend/src/app/layout.tsx | 28 +++++++++---- cannamanage-frontend/src/middleware.ts | 9 ++++- scripts/debug/.gitignore | 4 ++ scripts/debug/dashboard-probe.mjs | 54 +++++++++++++++++++++++++ scripts/debug/package.json | 15 +++++++ 6 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 scripts/debug/.gitignore create mode 100644 scripts/debug/dashboard-probe.mjs create mode 100644 scripts/debug/package.json diff --git a/cannamanage-frontend/public/sw.js b/cannamanage-frontend/public/sw.js index d3ab16c..1230d4e 100644 --- a/cannamanage-frontend/public/sw.js +++ b/cannamanage-frontend/public/sw.js @@ -1,6 +1,10 @@ /// -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 diff --git a/cannamanage-frontend/src/app/layout.tsx b/cannamanage-frontend/src/app/layout.tsx index 39af928..94502be 100644 --- a/cannamanage-frontend/src/app/layout.tsx +++ b/cannamanage-frontend/src/app/layout.tsx @@ -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 ( - - {children} - - - - - + + + {children} + + + + + + ) diff --git a/cannamanage-frontend/src/middleware.ts b/cannamanage-frontend/src/middleware.ts index 26b5038..49a2993 100644 --- a/cannamanage-frontend/src/middleware.ts +++ b/cannamanage-frontend/src/middleware.ts @@ -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).*)", ], } diff --git a/scripts/debug/.gitignore b/scripts/debug/.gitignore new file mode 100644 index 0000000..8b9a6cb --- /dev/null +++ b/scripts/debug/.gitignore @@ -0,0 +1,4 @@ +# Debug probe deps — installed on demand via `npm install playwright` +node_modules/ +package-lock.json +*.png diff --git a/scripts/debug/dashboard-probe.mjs b/scripts/debug/dashboard-probe.mjs new file mode 100644 index 0000000..8785607 --- /dev/null +++ b/scripts/debug/dashboard-probe.mjs @@ -0,0 +1,54 @@ +// Headless-browser probe: logs in and captures the REAL client-side error on /dashboard. +// Run via the official Playwright Docker image (no local Node needed): +// docker run --rm --network host -v "$PWD/scripts/debug:/work" -w /work \ +// mcr.microsoft.com/playwright:v1.49.0-jammy \ +// sh -c "npm i playwright@1.49.0 -s && node dashboard-probe.mjs" +// +// Captures: console messages, page errors (un-minified via sourcemaps if present), +// failed requests, and a screenshot. + +import { chromium } from "playwright" + +const BASE = process.env.BASE_URL || "http://192.168.188.119:3000" +const EMAIL = process.env.LOGIN_EMAIL || "admin@test.de" +const PASSWORD = process.env.LOGIN_PASSWORD || "test123" + +const browser = await chromium.launch() +const ctx = await browser.newContext({ ignoreHTTPSErrors: true }) +const page = await ctx.newPage() + +const log = [] +page.on("console", (m) => log.push(`[console.${m.type()}] ${m.text()}`)) +page.on("pageerror", (e) => log.push(`[pageerror] ${e.stack || e.message}`)) +page.on("requestfailed", (r) => + log.push(`[requestfailed] ${r.method()} ${r.url()} — ${r.failure()?.errorText}`) +) +page.on("response", (r) => { + if (r.status() >= 400) log.push(`[http ${r.status()}] ${r.url()}`) +}) + +try { + console.log(`==> GET ${BASE}/login`) + await page.goto(`${BASE}/login`, { waitUntil: "networkidle", timeout: 20000 }) + + await page.fill('input[type="email"], input[name="email"], #email', EMAIL) + await page.fill('input[type="password"], input[name="password"], #password', PASSWORD) + await page.click('button[type="submit"]') + + // Wait for navigation to settle on the dashboard (or wherever it lands) + await page.waitForTimeout(5000) + console.log(`==> landed on: ${page.url()}`) + + // Force-navigate to dashboard to reproduce the crash deterministically + await page.goto(`${BASE}/dashboard`, { waitUntil: "networkidle", timeout: 20000 }) + await page.waitForTimeout(3000) + + await page.screenshot({ path: "dashboard-probe.png", fullPage: true }) + console.log("==> screenshot saved: dashboard-probe.png") +} catch (e) { + console.log(`[probe-error] ${e.message}`) +} finally { + console.log("\n===== CAPTURED BROWSER EVENTS =====") + console.log(log.join("\n") || "(none)") + await browser.close() +} diff --git a/scripts/debug/package.json b/scripts/debug/package.json new file mode 100644 index 0000000..7c1a22f --- /dev/null +++ b/scripts/debug/package.json @@ -0,0 +1,15 @@ +{ + "name": "debug", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "playwright": "^1.60.0" + } +}