App versioning + the create page's missing top padding
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.
This commit is contained in:
parent
1f2e702b4e
commit
aac6befb0d
12 changed files with 124 additions and 10 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export default async function CreatePage({
|
|||
<SidebarOffset />
|
||||
<main className="flex min-h-dvh min-w-0 flex-1 flex-col">
|
||||
<AppHeader />
|
||||
<div className="mx-auto w-full max-w-3xl flex-1 px-4 pb-tabbar lg:px-10 lg:pb-16">
|
||||
<div className="mx-auto w-full max-w-3xl flex-1 px-4 pb-tabbar lg:px-10 lg:pb-16 lg:pt-10">
|
||||
<WizardModeSwitch mode="deck" />
|
||||
<RouteTransition>
|
||||
<UploadScreen />
|
||||
|
|
|
|||
|
|
@ -518,11 +518,13 @@ export function SettingsDialog({
|
|||
{version ? (
|
||||
<DetailRow
|
||||
label={t("settings.version")}
|
||||
value={
|
||||
version.builtAt
|
||||
? `${version.short} · ${new Date(version.builtAt).toLocaleString()}`
|
||||
: version.short
|
||||
}
|
||||
value={[
|
||||
version.version ? `v${version.version}` : "",
|
||||
version.short,
|
||||
version.builtAt ? new Date(version.builtAt).toLocaleString() : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
mono
|
||||
/>
|
||||
) : null}
|
||||
|
|
@ -797,7 +799,11 @@ export function SettingsDialog({
|
|||
{health ? <HealthPanel health={health} /> : null}
|
||||
{version ? (
|
||||
<p className="mt-2 text-[11px] text-[var(--text-subtle)]">
|
||||
{t("settings.version")}: <span className="font-mono">{version.short}</span>
|
||||
{t("settings.version")}:{" "}
|
||||
<span className="font-mono">
|
||||
{version.version ? `v${version.version} · ` : ""}
|
||||
{version.short}
|
||||
</span>
|
||||
{version.builtAt ? ` · ${new Date(version.builtAt).toLocaleString()}` : ""}
|
||||
</p>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
9
lib/version.ts
Normal file
9
lib/version.ts
Normal file
|
|
@ -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;
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
||||
|
|
|
|||
41
scripts/bump-version.mjs
Normal file
41
scripts/bump-version.mjs
Normal file
|
|
@ -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}`);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue