mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(dashboard): tile strip back to CSS Grid for true symmetric width
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) <noreply@anthropic.com>
This commit is contained in:
1 parent
cbded7c39e
commit
e8f4820018
5 files changed
+69
-23
No files matched your search
@@ -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 '<', "<!DOCTYPE "`. 401/403 from the provider now map to 422,
|
||||
429 to 429, and only genuine 5xx upstream errors keep the 502 —
|
||||
Cloudflare passes 4xx through untouched, so the React Query
|
||||
mutation reads the JSON body and surfaces a readable message.
|
||||
|
||||
### Fixed — Dashboard
|
||||
|
||||
- **Dashboard tiles use a CSS Grid layout again** — the v1.3-era
|
||||
pattern that gave each tile an identical width with symmetric gaps
|
||||
and an edge-to-edge fit with the charts below. v1.4.4 distributed
|
||||
width via `flex-1 basis-0`; CSS Grid is more honest about the
|
||||
intent and survives viewport changes more gracefully.
|
||||
`grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr))` keeps
|
||||
every track equal-width with a 9 rem floor; the row wraps when
|
||||
the floor no longer fits instead of triggering a horizontal
|
||||
scroll.
|
||||
|
||||
## [1.4.4] — 2026-05-08
|
||||
|
||||
### Fixed — Dashboard
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "healthlog",
|
||||
"version": "1.4.4",
|
||||
"version": "1.4.5",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.31.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -94,7 +94,11 @@ describe("POST /api/ai/test — provider error leak guard", () => {
|
||||
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",
|
||||
);
|
||||
|
||||
@@ -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 '<', "<!DOCTYPE "`,
|
||||
// which is exactly how Marc encountered this when typing a bad
|
||||
// OpenAI key. 4xx codes pass through untouched.
|
||||
const status = err.httpStatus ?? 0;
|
||||
const safeMessage =
|
||||
status === 401 || status === 403
|
||||
? "Provider rejected the credentials"
|
||||
: status === 429
|
||||
? "Provider rate-limited the request"
|
||||
: status >= 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);
|
||||
}
|
||||
});
|
||||
+12
-11
@@ -881,21 +881,22 @@ export default function DashboardPage() {
|
||||
* touch.
|
||||
*/}
|
||||
<div
|
||||
className="-mx-2 flex snap-x snap-mandatory items-stretch gap-3 overflow-x-auto px-2 pb-2 md:mx-0 md:px-0"
|
||||
// CSS Grid with `auto-fit + minmax(9rem, 1fr)` is the v1.4.4
|
||||
// attempt's flex-strip replacement: every tile gets EXACTLY
|
||||
// the same width (1fr each in the row's track list), the gap
|
||||
// is symmetric, and the strip starts and ends at the same
|
||||
// x-coordinates as the charts below because both inherit the
|
||||
// same parent container. When the row no longer fits a 9rem
|
||||
// floor, the grid wraps to a new row instead of horizontal-
|
||||
// scrolling — Marc tested both and prefers the v1.3-era
|
||||
// wrap behaviour over the one-row scroll for the symmetry
|
||||
// it preserves.
|
||||
className="grid auto-rows-fr gap-3 [grid-template-columns:repeat(auto-fit,minmax(9rem,1fr))] pb-2"
|
||||
data-slot="dashboard-tile-strip"
|
||||
data-tile-count={trendCards.length}
|
||||
>
|
||||
{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.
|
||||
<div
|
||||
key={entry.id}
|
||||
className="flex min-w-[9rem] flex-1 basis-0 snap-start"
|
||||
>
|
||||
<div key={entry.id} className="flex">
|
||||
{entry.node}
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in new issue
Block a user