From d1a52a66e0b5f4eccf42b5c52df9754d1b12e92f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 06:49:23 +0000 Subject: [PATCH 1/2] feat(web): display app version in the sidebar, sourced from version.txt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #458. Fixes a latent bug along the way: web/src/api/errorReport.ts has always read import.meta.env.VITE_APP_VERSION for crash reports, but nothing ever set it — in CI or the Dockerfile — so every crash report, including from production, silently sent appVersion: undefined. - web/.env.production (new, .gitignore negated — VITE_APP_VERSION is a public string baked into the client bundle regardless of whether it's committed) carries VITE_APP_VERSION, loaded automatically by Vite for a production build. No Dockerfile/CI plumbing needed beyond the file itself existing. - release-please-config.json gains an extra-files entry (the `generic` updater, anchored by an x-release-please-version marker on the line above the value — not a trailing comment, since dotenv-style parsers don't strip inline # comments) so future release PRs bump this file in the same PR as version.txt/the manifest. - AppLayout.tsx renders "v{{version}}" in the sidebar footer, alongside the theme toggle/sign-out — absent entirely in dev/test builds (the var is unset there), rather than showing a broken "v" or "vundefined". Verified end-to-end via a real `npm run build`: the built bundle contains `0.0.2` baked into both errorReport.ts's appVersion and AppLayout.tsx's APP_VERSION, confirming the whole pipeline works, not just the unit test. New test mutation-checked: the guard against a missing VITE_APP_VERSION was verified against the ACTUAL i18next behavior (a bare "v" — i18next renders a missing interpolation as empty, not string-concatenated "vundefined" as first assumed and tested for, which would have silently passed against the real bug). Could not verify the release-please extra-files wiring itself against a real release-please dry run (no network access to test the action) — this is the one part of the change that needs confirming on the next actual release PR: that web/.env.production's version line moves in lockstep with version.txt. --- .gitignore | 4 ++++ docs/decisions/351-releases.md | 9 +++++++++ release-please-config.json | 6 ++++++ web/.env.production | 2 ++ web/src/i18n/en.ts | 4 ++++ web/src/i18n/es.ts | 1 + web/src/i18n/tl.ts | 1 + web/src/routes/AppLayout.test.tsx | 15 +++++++++++++++ web/src/routes/AppLayout.tsx | 8 ++++++++ web/src/styles.css | 8 ++++++++ 10 files changed, 58 insertions(+) create mode 100644 web/.env.production diff --git a/.gitignore b/.gitignore index c49ef32f..b2e0bbc2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ obj/ .env .env.* !deploy/.env.example +# #458 — VITE_APP_VERSION only, a public string baked into the client bundle +# regardless of whether it's committed; release-please bumps it in lockstep +# with version.txt (see release-please-config.json's extra-files). +!web/.env.production appsettings.Development.local.json # OS/editor noise diff --git a/docs/decisions/351-releases.md b/docs/decisions/351-releases.md index eb641f12..02fd2edc 100644 --- a/docs/decisions/351-releases.md +++ b/docs/decisions/351-releases.md @@ -362,6 +362,15 @@ Two stages, deliberately separate: **CI publishes, the release PR versions.** `.release-please-manifest.json`, and `version.txt`. **Never hand-edit the manifest or `version.txt`**; release-please owns them and a manual edit desynchronises the version it believes from the tags that exist. +- **`extra-files` (#458) extends that ownership to `web/.env.production`'s + `VITE_APP_VERSION`** — release-please's `generic` updater bumps it in the same + release PR as `version.txt`/the manifest, anchored by an `x-release-please-version` + marker comment on the line above (not a same-line trailing comment — dotenv-style + parsers don't strip inline `#` comments the way YAML/generic key:value formats do, + so a trailing marker would have become part of the value). Vite loads + `.env.production` automatically for a production build; no Dockerfile/CI plumbing + needed beyond the file itself. Same "never hand-edit" rule applies to the value — + only the marker's presence and the file's existence are this repo's to maintain. - **The release PR is opened with a GitHub App token, not `GITHUB_TOKEN`** — and both reasons are load-bearing, so do not "simplify" it back. 1. `GITHUB_TOKEN` **cannot open a pull request at all** unless the repo-wide diff --git a/release-please-config.json b/release-please-config.json index f4dde593..5c127128 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -11,6 +11,12 @@ "bump-patch-for-minor-pre-major": true, "changelog-path": "CHANGELOG.md", "include-component-in-tag": false, + "extra-files": [ + { + "type": "generic", + "path": "web/.env.production" + } + ], "changelog-sections": [ { "type": "feat", "section": "Features" }, { "type": "fix", "section": "Bug fixes" }, diff --git a/web/.env.production b/web/.env.production new file mode 100644 index 00000000..4a7422c7 --- /dev/null +++ b/web/.env.production @@ -0,0 +1,2 @@ +# x-release-please-version +VITE_APP_VERSION=0.0.2 diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index e6e5b148..ec480a23 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -170,6 +170,10 @@ export const en = { skipToContent: "Skip to main content", primaryNavAriaLabel: "Primary", signOut: "Sign out", + // {{version}} is a bare semver string (e.g. "0.0.2"), never itself + // translated — only the surrounding "v" template varies by locale in + // principle, though every catalog currently agrees on it (#458). + versionLabel: "v{{version}}", farmLoadFailedNeverLoaded: "Could not load this farm's settings, so dates follow this device rather than the farm.", farmLoadFailedStale: diff --git a/web/src/i18n/es.ts b/web/src/i18n/es.ts index 8b03b2cd..d96749ed 100644 --- a/web/src/i18n/es.ts +++ b/web/src/i18n/es.ts @@ -138,6 +138,7 @@ export const es = { skipToContent: "Saltar al contenido principal", primaryNavAriaLabel: "Principal", signOut: "Cerrar sesión", + versionLabel: "v{{version}}", farmLoadFailedNeverLoaded: "No se pudo cargar la configuración de esta granja, así que las " + "fechas siguen a este dispositivo en lugar de a la granja.", diff --git a/web/src/i18n/tl.ts b/web/src/i18n/tl.ts index 18cd702b..39eaefd4 100644 --- a/web/src/i18n/tl.ts +++ b/web/src/i18n/tl.ts @@ -148,6 +148,7 @@ export const tl = { skipToContent: "Lumaktaw papunta sa pangunahing content", primaryNavAriaLabel: "Pangunahin", signOut: "Mag-sign out", + versionLabel: "v{{version}}", farmLoadFailedNeverLoaded: "Hindi na-load ang mga setting ng bukid na ito, kaya susundin ng mga " + "petsa ang device na ito sa halip na ang bukid.", diff --git a/web/src/routes/AppLayout.test.tsx b/web/src/routes/AppLayout.test.tsx index 4f38758f..bdd703a8 100644 --- a/web/src/routes/AppLayout.test.tsx +++ b/web/src/routes/AppLayout.test.tsx @@ -103,6 +103,21 @@ describe("AppLayout sidebar", () => { expect(screen.queryByRole("alert")).not.toBeInTheDocument(); }); + it("shows no version line when VITE_APP_VERSION is unset (#458 — dev/test builds)", () => { + // import.meta.env.VITE_APP_VERSION is read once at module scope + // (AppLayout.tsx), matching errorReport.ts's own "absent in dev builds" + // contract — this test environment never sets it, so this proves the + // component renders nothing rather than a literal "vundefined". + renderWithProviders(, { token: { sub: "u1", role: "Admin" } }); + // Matches either a real version ("v0.0.2") or i18next's literal rendering + // of a missing interpolation (a bare "v", the {{version}} slot rendering + // empty) — both must be absent. Measured directly: i18next does not + // render "vundefined" for a missing var, it renders an empty + // interpolation, so a regex assuming the JS-string-concat shape would + // have silently passed against the exact bug this guards. + expect(screen.queryByText(/^v(\d.*)?$/)).not.toBeInTheDocument(); + }); + it("toggles light ↔ night, flipping the control and the root data-theme", () => { renderWithProviders(, { token: { sub: "u1", role: "Admin" } }); // jsdom has no matchMedia → initial theme resolves to light, so the control diff --git a/web/src/routes/AppLayout.tsx b/web/src/routes/AppLayout.tsx index ad7b13cb..06219747 100644 --- a/web/src/routes/AppLayout.tsx +++ b/web/src/routes/AppLayout.tsx @@ -12,6 +12,10 @@ import { navGroups, tabEntries } from "./nav"; const ICON = 17; +// #458 — same env var, same "absent in dev" contract errorReport.ts already +// relies on for crash reports; read once at module scope rather than per render. +const APP_VERSION = import.meta.env.VITE_APP_VERSION as string | undefined; + // Authenticated shell (#52 redesign): an aubergine sidebar — the brand's // navigation spine — with the 15+ destinations grouped by job, each with a // lucide glyph. Role-tiered (#103): links and whole groups hide per role; the @@ -73,6 +77,10 @@ export function AppLayout() { + {/* #458 — set at build time (VITE_APP_VERSION, release-please-owned + via web/.env.production); absent in dev builds, so this line + simply doesn't render rather than showing "vundefined". */} + {APP_VERSION &&

{t("versionLabel", { version: APP_VERSION })}

} diff --git a/web/src/styles.css b/web/src/styles.css index 69dce497..39db25d0 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1223,6 +1223,14 @@ input.cell { text-decoration: none; } +.sidebar-version { + margin: 0.35rem 0 0.6rem; + padding: 0 0.25rem; + font-size: 0.75rem; + color: var(--on-brand-mute); + opacity: 0.7; +} + /* -------------------------------------------- Bottom nav (mobile) */ /* The tab bar and its More sheet own navigation below 900px; the sidebar owns From a09619280b03607c5e8f6ef7b43a980b0e9af2fa Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 06:54:09 +0000 Subject: [PATCH 2/2] fix: match web/.env.production's seed version to version.txt (codex review) version.txt/the manifest moved to 0.0.3 on main since this branch was cut; 0.0.2 would have shipped in every image built from this commit until a later release PR happened to correct it (affects CI's per-merge images, not just a local build). --- web/.env.production | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/.env.production b/web/.env.production index 4a7422c7..16acae7f 100644 --- a/web/.env.production +++ b/web/.env.production @@ -1,2 +1,2 @@ # x-release-please-version -VITE_APP_VERSION=0.0.2 +VITE_APP_VERSION=0.0.3