From fbce838d3f44e7c731b0fff92a3f21eea5e63600 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 14 Jan 2026 21:47:31 +0000 Subject: [PATCH] fix(consent): avoid banner flashing on reload Initialize consent state from cookie synchronously so the banner only shows when no choice was made. fix(api): fail-soft when DB schema missing Return null/empty content for CMS endpoints when migrations are not applied instead of crashing with Prisma P2021/P2022. fix(n8n): parse status response defensively Handle empty/invalid JSON bodies from n8n to prevent activity feed from getting stuck. Co-authored-by: dennis --- app/api/content/page/route.ts | 15 +++++++--- app/api/n8n/status/route.ts | 19 ++++++++++-- app/components/ConsentProvider.tsx | 9 ++---- lib/content.ts | 46 ++++++++++++++++++------------ 4 files changed, 59 insertions(+), 30 deletions(-) diff --git a/app/api/content/page/route.ts b/app/api/content/page/route.ts index 8294021..0ebd757 100644 --- a/app/api/content/page/route.ts +++ b/app/api/content/page/route.ts @@ -10,9 +10,16 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: "key is required" }, { status: 400 }); } - const translation = await getContentByKey({ key, locale }); - if (!translation) return NextResponse.json({ content: null }); - - return NextResponse.json({ content: translation }); + try { + const translation = await getContentByKey({ key, locale }); + if (!translation) return NextResponse.json({ content: null }); + return NextResponse.json({ content: translation }); + } catch (error) { + // If DB isn't migrated/available, fail soft so the UI can fall back to next-intl strings. + if (process.env.NODE_ENV === "development") { + console.warn("Content API failed; returning null content:", error); + } + return NextResponse.json({ content: null }); + } } diff --git a/app/api/n8n/status/route.ts b/app/api/n8n/status/route.ts index 34ff34a..45aab8a 100644 --- a/app/api/n8n/status/route.ts +++ b/app/api/n8n/status/route.ts @@ -54,7 +54,8 @@ export async function GET(request: NextRequest) { const res = await fetch(statusUrl, { method: "GET", headers: { - "Content-Type": "application/json", + // n8n sometimes responds with empty body; we'll parse defensively below. + Accept: "application/json", ...(process.env.N8N_SECRET_TOKEN && { Authorization: `Bearer ${process.env.N8N_SECRET_TOKEN}`, }), @@ -71,7 +72,21 @@ export async function GET(request: NextRequest) { throw new Error(`n8n error: ${res.status} - ${errorText}`); } - const data = await res.json(); + const raw = await res.text().catch(() => ""); + if (!raw || !raw.trim()) { + throw new Error("Empty response body received from n8n"); + } + + let data: unknown; + try { + data = JSON.parse(raw); + } catch (parseError) { + // Sometimes upstream sends HTML or a partial response; include a snippet for debugging. + const snippet = raw.slice(0, 240); + throw new Error( + `Invalid JSON from n8n (${res.status}): ${snippet}${raw.length > 240 ? "…" : ""}`, + ); + } // n8n gibt oft ein Array zurück: [{...}]. Wir wollen nur das Objekt. const statusData = Array.isArray(data) ? data[0] : data; diff --git a/app/components/ConsentProvider.tsx b/app/components/ConsentProvider.tsx index 7a3cea0..804ea62 100644 --- a/app/components/ConsentProvider.tsx +++ b/app/components/ConsentProvider.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; +import React, { createContext, useCallback, useContext, useMemo, useState } from "react"; export type ConsentState = { analytics: boolean; @@ -46,11 +46,8 @@ const ConsentContext = createContext<{ }); export function ConsentProvider({ children }: { children: React.ReactNode }) { - const [consent, setConsentState] = useState(null); - - useEffect(() => { - setConsentState(readConsentFromCookie()); - }, []); + // Read cookie synchronously so we don't flash the banner on every reload. + const [consent, setConsentState] = useState(() => readConsentFromCookie()); const setConsent = useCallback((next: ConsentState) => { setConsentState(next); diff --git a/lib/content.ts b/lib/content.ts index 68888ed..5521b0d 100644 --- a/lib/content.ts +++ b/lib/content.ts @@ -1,5 +1,6 @@ import { prisma } from "@/lib/prisma"; import type { Prisma } from "@prisma/client"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; export async function getSiteSettings() { return prisma.siteSettings.findUnique({ where: { id: 1 } }); @@ -7,29 +8,38 @@ export async function getSiteSettings() { export async function getContentByKey(opts: { key: string; locale: string }) { const { key, locale } = opts; - const page = await prisma.contentPage.findUnique({ - where: { key }, - include: { - translations: { - where: { locale }, - take: 1, + try { + const page = await prisma.contentPage.findUnique({ + where: { key }, + include: { + translations: { + where: { locale }, + take: 1, + }, }, - }, - }); + }); - if (page?.translations?.[0]) return page.translations[0]; + if (page?.translations?.[0]) return page.translations[0]; - const settings = await getSiteSettings(); - const fallbackLocale = settings?.defaultLocale || "en"; + const settings = await getSiteSettings(); + const fallbackLocale = settings?.defaultLocale || "en"; - const fallback = await prisma.contentPageTranslation.findFirst({ - where: { - page: { key }, - locale: fallbackLocale, - }, - }); + const fallback = await prisma.contentPageTranslation.findFirst({ + where: { + page: { key }, + locale: fallbackLocale, + }, + }); - return fallback; + return fallback; + } catch (error) { + // If migrations haven't been applied yet, don't crash the app. + // Let callers fall back to static translations. + if (error instanceof PrismaClientKnownRequestError && (error.code === "P2021" || error.code === "P2022")) { + return null; + } + throw error; + } } export async function upsertContentByKey(opts: {