mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
The measured first-load gap on / was never the server: FCP painted the SSR shell at ~1.8 s but LCP landed at ~5.5 s (mobile-throttled) because the tiles waited for JS download + hydrate + a CLIENT-side snapshot fetch — while /api/dashboard/snapshot answers in ~8 ms server-side. src/app/page.tsx is now a thin RSC wrapper: it runs the same cached snapshot read the API route uses (extracted to src/lib/dashboard/snapshot-read.ts, one shared SWR cell for both readers) and hands the result to TanStack via HydrationBoundary + dehydrate, so the first HTML carries real tile data and the mounted cell starts warm (staleTime 60 s, refetchOnMount false — no double fetch). Query keys come only from the central factory (dashboardSnapshot(locale) + the dashboardWidgets seed the client cell already performs); the payload is JSON-round-tripped so the hydrated shape is byte-equal to the wire shape. Fail-soft: no session or any builder error renders the page exactly as before. The 1,727-line client page moves unchanged to src/app/page-client.tsx; no component-tree rewrite. Companion: the documented bundle tooling was dead on Turbopack (ANALYZE=1 silent no-op, bundle-report read a webpack-only JSON), so H2/H3-class regressions shipped invisibly. scripts/check-bundle-budget.mjs derives per-route gzip totals from the Turbopack client-reference manifests + rootMainFiles, prints the report (pnpm bundle-report), and enforces bundle-budget.json (pnpm bundle-check): per-route caps, total-client-JS cap, a recharts-duplication guard (fingerprint count must stay 1), and a catalog guard (no message catalog referenced by any route manifest). Wired into the existing e2e workflow after next build. H4
60 lines
2.7 KiB
TypeScript
60 lines
2.7 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
/**
|
|
* v1.4.39.2 — dashboard analytics slim/thick split.
|
|
*
|
|
* Pre-fix the dashboard fired a single `useAnalyticsQuery()` against the
|
|
* thick `/api/analytics` envelope; every per-type tile waited on the
|
|
* heavy fan-out before paint. Mood and medication tiles arrived first
|
|
* (separate routes) and then every other tile arrived as one burst,
|
|
* which the maintainer reported as "etwas nervig" in the v1.4.39.1 post-deploy
|
|
* trace.
|
|
*
|
|
* Post-fix the dashboard mounts TWO `useAnalyticsQuery` calls in
|
|
* parallel: one for `?slice=summaries` (per-type tile strip) and one
|
|
* for the thick envelope (BD-Zielbereich + glucose tiles). The tile
|
|
* strip paints from the slim slice as soon as it lands; thick fields
|
|
* stream in afterwards. Without this dual-mount the cold-mount UX
|
|
* waterfall regresses to the v1.4.39.1 behaviour, so the test pins the
|
|
* two call shapes.
|
|
*
|
|
* v1.7.0 W6 — both calls now carry an `enabled` gate so the unified
|
|
* snapshot rollout flag can disable them in favour of a single
|
|
* `/api/dashboard/snapshot` cell. With the flag OFF (default) the
|
|
* slim/thick split still drives the page, so the protective intent
|
|
* holds; the matchers only relaxed to tolerate the `enabled` arg.
|
|
*/
|
|
const ROOT = join(__dirname, "../../..");
|
|
const PAGE_PATH = join(ROOT, "src/app/page-client.tsx");
|
|
|
|
function load(path: string): string {
|
|
return readFileSync(path, "utf8");
|
|
}
|
|
|
|
describe("v1.4.39.2 — dashboard analytics slim/thick split", () => {
|
|
it("mounts both the slim slice and the thick slice in parallel", () => {
|
|
const src = load(PAGE_PATH);
|
|
// Slim slice — `?slice=summaries` paints the per-type tile strip.
|
|
expect(src).toMatch(/useAnalyticsQuery\(\{\s*\n?\s*slice:\s*"summaries"/);
|
|
// Thick slice — the no-slice `useAnalyticsQuery` feeds the
|
|
// BD-Zielbereich + glucose tiles. Both must coexist. The thick
|
|
// call now opens with `{ enabled:` (no `slice` key).
|
|
expect(src).toMatch(/useAnalyticsQuery\(\{\s*\n?\s*enabled:/);
|
|
});
|
|
|
|
it("merges slim and thick results so call-sites stay shape-stable", () => {
|
|
const src = load(PAGE_PATH);
|
|
// The merge contract: slim wins on overlapping fields, thick fills
|
|
// in `bpInTargetPct*` and `glucoseByContext`. The text anchors
|
|
// protect against a future refactor accidentally dropping one of
|
|
// the queries — the `data` object that downstream tile rendering
|
|
// consumes must stay shape-stable across both query states.
|
|
expect(src).toMatch(/analyticsSlimQuery/);
|
|
expect(src).toMatch(/analyticsThickQuery/);
|
|
expect(src).toMatch(/bpInTargetPct/);
|
|
expect(src).toMatch(/glucoseByContext/);
|
|
});
|
|
});
|