feat(setup): show running build version in the Setup tab bar
Build desktop / desktop (push) Successful in 4m21s
Build & push images / images (push) Successful in 3m6s
CI / check (push) Successful in 42s

CI already computes <branch>-<short-sha> for image tags but never
surfaced it anywhere reachable from the app, so there was no way to
tell what's actually deployed on a booth without cross-referencing
komodo/resources.toml's TAG by hand.

Thread it through: CI passes BUILD_VERSION as a Docker build-arg,
the Dockerfile captures it as a runtime env var, GET /api/version
(gated by the existing site:read permission) exposes it, and the
Setup page's tab bar shows it right-aligned, muted, absent entirely
on a local/dev build with no CI-supplied value.

Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
2026-08-30 19:01:15 +02:00
parent cb9f4d4979
commit 642c5f4f70
6 changed files with 69 additions and 0 deletions
+31
View File
@@ -56,6 +56,37 @@ describe("auth guard — no token", () => {
});
});
describe("GET /api/version", () => {
it("without a session is 401", async () => {
const res = await app.inject({ method: "GET", url: "/api/version" });
expect(res.statusCode).toBe(401);
});
it("a site:read user gets the BUILD_VERSION env var, null when unset", async () => {
const { username, password } = await seedUser(db, {
username: "viewer2", roleId: "viewer2", permissions: ["site:read"],
});
const { cookie } = await login(app, username, password);
const res = await app.inject({ method: "GET", url: "/api/version", headers: { cookie } });
expect(res.statusCode).toBe(200);
expect(res.json()).toEqual({ buildVersion: null }); // no BUILD_VERSION set in the test env
});
it("reflects a real BUILD_VERSION when the env var is set", async () => {
process.env.BUILD_VERSION = "stage-abc1234";
try {
const { username, password } = await seedUser(db, {
username: "viewer3", roleId: "viewer3", permissions: ["site:read"],
});
const { cookie } = await login(app, username, password);
const res = await app.inject({ method: "GET", url: "/api/version", headers: { cookie } });
expect(res.json()).toEqual({ buildVersion: "stage-abc1234" });
} finally {
delete process.env.BUILD_VERSION;
}
});
});
describe("RBAC permission gate", () => {
it("a site:read-only user can GET occupancy but is 403 on PUT site-config", async () => {
const { username, password } = await seedUser(db, {
+9
View File
@@ -78,6 +78,15 @@ export async function siteRoutes(app: FastifyInstance, db: Db, eventLog?: EventL
// Live occupancy: cars inside, capacity, free, full. Any signed-in role.
app.get("/api/occupancy", { preHandler: readGuard }, async () => getOccupancy(db));
// Running build version ("<branch>-<short-sha>", matching the Komodo Stack's TAG in
// komodo/resources.toml) — baked in at image build time (apps/server/Dockerfile
// BUILD_VERSION ARG), read here from the running process env. null on a local/dev
// build with no CI-supplied value. Purely informational (Setup nav display); not
// site config, so it isn't stored in site_config.
app.get("/api/version", { preHandler: readGuard }, async () => ({
buildVersion: process.env.BUILD_VERSION?.trim() || null,
}));
// Read site config (capacity + park metadata).
app.get("/api/site-config", { preHandler: readGuard }, async () => {
const row = db.select().from(siteConfig).where(eq(siteConfig.id, 1)).get();