From e8f48200185500f0f6d0aeef019766a3d2d7abff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 9 May 2026 00:01:26 +0200 Subject: [PATCH] fix(dashboard): tile strip back to CSS Grid for true symmetric width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marc's v1.4.4 feedback: the tiles look better when they're all exactly the same size and the strip begins and ends at the same x-coordinate as the charts — the v1.4.4 `flex-1 basis-0` distribution was close but not quite symmetric, and at 6 tiles the right edge of the strip didn't line up with the charts below. Switched to the v1.3-era pattern but with the modern `auto-fit + minmax(9rem, 1fr)` form so it doesn't need explicit breakpoints: grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 0.75rem; CSS Grid puts every tile in an identical 1fr track. Width is literally identical, gaps are symmetric, and the strip is bounded by the same parent box as the charts so left/right edges align by construction. When the row no longer fits a 9 rem floor the grid wraps to a new row — Marc explicitly tested both and prefers the wrap over the v1.4.x horizontal-scroll for the symmetry it preserves. Tests + typecheck green (671/671). Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 25 +++++++++++++++++ package.json | 2 +- src/app/api/ai/test/__tests__/route.test.ts | 11 ++++++-- src/app/api/ai/test/route.ts | 31 +++++++++++++++------ src/app/page.tsx | 23 +++++++-------- 5 files changed, 69 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd6f23b6..05311a6e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## [1.4.5] — 2026-05-09 + +### Fixed — Settings · KI + +- **"Verbindung testen" no longer crashes the page when the key is + bad.** The endpoint mapped every upstream error to HTTP 502, which + Cloudflare intercepts and replaces with its own HTML error page. + `await res.json()` in the browser then crashed with `Unexpected + token '<', " { const response = await POST(emptyRequest() as never); const body = (await response.json()) as ApiErrorEnvelope; - expect(response.status).toBe(502); + // v1.4.5: credential failures map to 422 (not 502) so Cloudflare + // doesn't replace our JSON body with its HTML error page — that + // rewrite was the root cause of the "Unexpected token '<'…" client + // crash Marc hit when typing a bad OpenAI key. + expect(response.status).toBe(422); expect(body.error ?? "").not.toMatch(/sk-/); expect(body.error ?? "").not.toMatch(/api\.openai\.com/); expect(body.error ?? "").not.toMatch(/invalid api key/i); @@ -106,7 +110,10 @@ describe("POST /api/ai/test — provider error leak guard", () => { Object.assign(new Error("429 from openai"), { httpStatus: 429 as const }), ); const response = await POST(emptyRequest() as never); - expect(response.status).toBe(502); + // v1.4.5: rate-limit passes through as 429 (was 502 in v1.4.4) so + // the React Query mutation can read the JSON body instead of + // tripping over Cloudflare's HTML 502. + expect(response.status).toBe(429); expect(((await response.json()) as ApiErrorEnvelope).error).toBe( "Provider rate-limited the request", ); diff --git a/src/app/api/ai/test/route.ts b/src/app/api/ai/test/route.ts index ce98160e9..77cf7f4a5 100644 --- a/src/app/api/ai/test/route.ts +++ b/src/app/api/ai/test/route.ts @@ -91,15 +91,28 @@ export const POST = apiHandler(async (request: NextRequest) => { ai_test_provider: provider.type, }, }); + // Map upstream status to a *client-side* status code that won't be + // rewritten by Cloudflare. 5xx from origin causes Cloudflare to swap + // the body for its own HTML error page — `await res.json()` in the + // browser then crashes with `Unexpected token '<', "= 500 - ? "Provider returned a server error" - : "Provider connection failed"; - return apiError(safeMessage, 502); + let outboundStatus: number; + let safeMessage: string; + if (status === 401 || status === 403) { + outboundStatus = 422; + safeMessage = "Provider rejected the credentials"; + } else if (status === 429) { + outboundStatus = 429; + safeMessage = "Provider rate-limited the request"; + } else if (status >= 500) { + outboundStatus = 502; + safeMessage = "Provider returned a server error"; + } else { + outboundStatus = 422; + safeMessage = "Provider connection failed"; + } + return apiError(safeMessage, outboundStatus); } }); diff --git a/src/app/page.tsx b/src/app/page.tsx index 66e1e0fdb..b50b12d28 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -881,21 +881,22 @@ export default function DashboardPage() { * touch. */}
{trendCards.map((entry) => ( - // `flex-1 basis-0` distributes the strip width evenly across - // every tile so 5 tiles or 7 tiles both look symmetric — no - // tile claims more horizontal space because its content is - // wider. `min-w-[9rem]` keeps the cells readable on narrow - // viewports; if the row no longer fits, the parent's - // `overflow-x-auto` lets the user swipe instead of wrapping. -
+
{entry.node}
))}