From aac6befb0d2f9f5d8786e5c72264956f8d39cb47 Mon Sep 17 00:00:00 2001 From: Martin Dvorak Date: Mon, 12 Oct 2026 20:04:26 +0000 Subject: [PATCH] App versioning + the create page's missing top padding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Versioning: package.json carries the app version (0.1.2 here; one bump per task with `npm run version:bump`, patch 0-9 rolling into the minor at 10), baked into the build via lib/version.ts, reported by /api/healthz and shown in Settings next to the running commit. AGENTS.md documents the rule; the native suite checks both the API and the Settings row. Fix: on desktop the app header is hidden, so every page owns its top padding — /create was the only one without `lg:pt-10`, which slammed the Study deck / Make my homework switch against the top of the window while /homework had a comfortable gap. Both now sit at 40px; the wizard suite asserts the clearance and that the two pages stay in step (90 checks). SW -> freezmo-v50. --- AGENTS.md | 5 +++++ README.md | 1 + app/api/healthz/route.ts | 3 +++ app/create/page.tsx | 2 +- components/SettingsDialog.tsx | 18 ++++++++++----- lib/use-version.ts | 2 ++ lib/version.ts | 9 ++++++++ package.json | 5 +++-- public/sw.js | 2 +- scripts/bump-version.mjs | 41 +++++++++++++++++++++++++++++++++++ tests/native-test.mjs | 19 ++++++++++++++++ tests/wizard-test.mjs | 27 +++++++++++++++++++++++ 12 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 lib/version.ts create mode 100644 scripts/bump-version.mjs diff --git a/AGENTS.md b/AGENTS.md index 3195cbd..decd690 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,3 +54,8 @@ Production only ever runs a pushed, verified commit deployed by `tests/bubble-perf.mjs`, `tests/model-compare.mjs`. 10. **Keep the service worker honest**: bump `CACHE_VERSION` in `public/sw.js` for every user-visible deploy (the deploy itself does not do this). +11. **Version the app once per task.** Bump `package.json` `version` with + `npm run version:bump`: the patch digit goes 0-9 and after 9 it rolls + into the minor (`0.1.9` → `0.2.0`). The version is baked into the build + (`lib/version.ts`), reported by `/api/healthz` and shown in Settings next + to the running commit — so every task's release is identifiable. diff --git a/README.md b/README.md index 2db47e0..d80e47b 100644 --- a/README.md +++ b/README.md @@ -899,6 +899,7 @@ grading, regeneration and “add more” (and the assignment language for homewo | `npm run build` | production build (type-check + lint) | | `npm start` | `next start -H 0.0.0.0 -p 3207` | | `npm run lint` | ESLint | +| `npm run version:bump` | bump the app version one step (`0.1.7` → `0.1.8`, `0.1.9` → `0.2.0`); one bump per task | | `node scripts/make-icons.mjs` | regenerate the PWA/iOS icons (needs a Playwright Chromium) | | `node scripts/copy-pdf-worker.mjs` | copy the pdf.js worker (also runs on predev/prebuild) | diff --git a/app/api/healthz/route.ts b/app/api/healthz/route.ts index d52d2cb..e807bb3 100644 --- a/app/api/healthz/route.ts +++ b/app/api/healthz/route.ts @@ -1,6 +1,8 @@ import { readFileSync } from "node:fs"; import path from "node:path"; +import { APP_VERSION } from "@/lib/version"; + export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -25,6 +27,7 @@ export async function GET() { return Response.json( { ok: true, + version: APP_VERSION, commit, short: release?.short ?? commit.slice(0, 7), builtAt: release?.builtAt ?? null, diff --git a/app/create/page.tsx b/app/create/page.tsx index d228053..973dac1 100644 --- a/app/create/page.tsx +++ b/app/create/page.tsx @@ -28,7 +28,7 @@ export default async function CreatePage({
-
+
diff --git a/components/SettingsDialog.tsx b/components/SettingsDialog.tsx index 70a1b03..3096328 100644 --- a/components/SettingsDialog.tsx +++ b/components/SettingsDialog.tsx @@ -518,11 +518,13 @@ export function SettingsDialog({ {version ? ( ) : null} @@ -797,7 +799,11 @@ export function SettingsDialog({ {health ? : null} {version ? (

- {t("settings.version")}: {version.short} + {t("settings.version")}:{" "} + + {version.version ? `v${version.version} · ` : ""} + {version.short} + {version.builtAt ? ` · ${new Date(version.builtAt).toLocaleString()}` : ""}

) : null} diff --git a/lib/use-version.ts b/lib/use-version.ts index 9b1f753..f0562ec 100644 --- a/lib/use-version.ts +++ b/lib/use-version.ts @@ -3,6 +3,8 @@ import { useEffect, useState } from "react"; export type RunningVersion = { + /** App version from package.json (bumped once per task). */ + version?: string; commit: string; short: string; builtAt: string | null; diff --git a/lib/version.ts b/lib/version.ts new file mode 100644 index 0000000..efd2f59 --- /dev/null +++ b/lib/version.ts @@ -0,0 +1,9 @@ +import packageJson from "../package.json"; + +/** + * The app version from package.json, inlined at build time. One bump per task + * with `npm run version:bump`: patch 0-9, then it rolls into the minor + * (0.1.9 -> 0.2.0). Shown in Settings next to the running commit and reported + * by /api/healthz. + */ +export const APP_VERSION: string = packageJson.version; diff --git a/package.json b/package.json index 9edce0c..5528d2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "freezmo", - "version": "0.1.0", + "version": "0.1.2", "private": true, "license": "GPL-3.0-only", "scripts": { @@ -10,7 +10,8 @@ "build": "next build", "start": "next start -H 0.0.0.0 -p 3207", "lint": "eslint", - "test:e2e": "node tests/run-all.mjs" + "test:e2e": "node tests/run-all.mjs", + "version:bump": "node scripts/bump-version.mjs" }, "dependencies": { "canvas-confetti": "^1.9.4", diff --git a/public/sw.js b/public/sw.js index 6b8d422..6f0c56f 100644 --- a/public/sw.js +++ b/public/sw.js @@ -9,7 +9,7 @@ * * Bumping CACHE_VERSION is how a new deploy invalidates old assets. */ -const CACHE_VERSION = "freezmo-v49"; +const CACHE_VERSION = "freezmo-v50"; const SHELL_CACHE = `${CACHE_VERSION}-shell`; const ASSET_CACHE = `${CACHE_VERSION}-assets`; diff --git a/scripts/bump-version.mjs b/scripts/bump-version.mjs new file mode 100644 index 0000000..5dd6980 --- /dev/null +++ b/scripts/bump-version.mjs @@ -0,0 +1,41 @@ +#!/usr/bin/env node +/** + * Bump the app version one step: 0.1.7 -> 0.1.8, and 0.1.9 -> 0.2.0 — the + * patch digit never reaches two digits (the working rule is one bump per + * task; see AGENTS.md). + * + * npm run version:bump # one step up, prints old -> new + * npm run version:bump -- 0.3.0 # set an explicit version + * + * The version is baked into the build (lib/version.ts) and shown in Settings + * next to the running commit. + */ +import { readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +const file = path.join(import.meta.dirname, "..", "package.json"); +const raw = readFileSync(file, "utf8"); +const current = JSON.parse(raw).version; + +const explicit = process.argv[2]; +let next = explicit; + +if (!next) { + const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(current); + if (!match) { + console.error(`package.json version "${current}" is not x.y.z — pass one explicitly`); + process.exit(1); + } + const major = Number(match[1]); + const minor = Number(match[2]); + const patch = Number(match[3]); + next = patch >= 9 ? `${major}.${minor + 1}.0` : `${major}.${minor}.${patch + 1}`; +} + +if (!/^\d+\.\d+\.\d+$/.test(next)) { + console.error(`"${next}" is not a valid x.y.z version`); + process.exit(1); +} + +writeFileSync(file, raw.replace(`"version": "${current}"`, `"version": "${next}"`)); +console.log(`${current} -> ${next}`); diff --git a/tests/native-test.mjs b/tests/native-test.mjs index b3cd081..198212d 100644 --- a/tests/native-test.mjs +++ b/tests/native-test.mjs @@ -30,6 +30,25 @@ const browser = await launchBrowser(); await context.close(); } +/* 1b. the app version is reported and shown in Settings --------------- */ +{ + const { context, page } = await makePage(browser); + await seed(page, { decks: [] }); + await page.goto(BASE, { waitUntil: "networkidle" }); + await page.waitForTimeout(600); + const health = await page.evaluate(async () => (await fetch("/api/healthz", { cache: "no-store" })).json()); + check("healthz reports the app version", /^\d+\.\d+\.\d+$/.test(health.version ?? ""), String(health.version)); + await page.locator("nav.tabbar").getByRole("button").click(); + await page.getByRole("dialog").waitFor({ timeout: 6000 }); + await page.waitForTimeout(700); + check( + "settings shows the app version", + (await page.getByText(new RegExp(`v${String(health.version).replace(/\./g, "\\.")}`)).count()) >= 1, + String(health.version), + ); + await context.close(); +} + /* 2. offline: banner + generation disabled --------------------------- */ { const { context, page } = await makePage(browser); diff --git a/tests/wizard-test.mjs b/tests/wizard-test.mjs index 8dc6c7e..78b6dd6 100644 --- a/tests/wizard-test.mjs +++ b/tests/wizard-test.mjs @@ -115,6 +115,33 @@ for (const viewport of [{ width: 390, height: 844 }, { width: 360, height: 740 } await context.close(); } +/* 2b. the wizard clears the top edge on desktop ----------------------- */ +/* On desktop the app header is hidden (`lg:hidden`), so each page owns its + top padding. /create was missing it and the mode switch sat flush against + the top of the window while /homework had a comfortable gap. */ +{ + const { context, page } = await makePage(browser, { width: 1280, height: 900 }); + await seed(page, { decks: [] }); + const switcherTop = async () => { + await page.waitForTimeout(700); + return page.evaluate(() => { + const switcher = document.querySelector('[role="group"][aria-label="Study deck or homework"]'); + return switcher ? Math.round(switcher.getBoundingClientRect().top) : null; + }); + }; + await page.goto(`${BASE}/create`, { waitUntil: "networkidle" }); + const createTop = await switcherTop(); + check("create: the mode switch clears the top edge on desktop", createTop !== null && createTop >= 24, `${createTop}px`); + await page.goto(`${BASE}/homework`, { waitUntil: "networkidle" }); + const homeworkTop = await switcherTop(); + check( + "create and homework keep the same clearance", + homeworkTop !== null && homeworkTop >= 24 && createTop !== null && Math.abs(homeworkTop - createTop) <= 4, + `${createTop}px vs ${homeworkTop}px`, + ); + await context.close(); +} + /* 3. reduced motion: bar still pinned, wizard usable ----------------- */ { const { context, page } = await makePage(browser, { reducedMotion: "reduce" });