From 09e1bec9bb6389e55f55307ee3df885221e3166b Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 3 Aug 2026 14:38:07 +0200 Subject: [PATCH] client: show build version + browser diagnostics for bug triage Fixes #68. - Bar switcher and stats page get a low-contrast "wutzcalc " footer at the bottom (VersionFooter component, shared). - Admin panel gets a fuller Diagnose section: version, user agent, screen resolution + device pixel ratio, viewport size, browser language. Version is the short git commit sha, baked in at build time via vite.config.ts define (falls back to "dev" if .git is unavailable at build time, e.g. a tarball/CI-artifact deploy) - matches how deploys actually run (pnpm build from a git checkout, per deploy/wutzcalc.service), but stays defensive rather than failing the build. pnpm --filter client typecheck and build both clean. --- client/src/VersionFooter.tsx | 24 ++++++++++++++++++++++++ client/src/admin/Admin.tsx | 16 ++++++++++++++++ client/src/stats/Stats.tsx | 3 +++ client/src/styles.css | 29 +++++++++++++++++++++++++++++ client/src/tablet/BarPicker.tsx | 2 ++ client/src/vite-env.d.ts | 5 +++++ client/vite.config.ts | 17 +++++++++++++++++ 7 files changed, 96 insertions(+) create mode 100644 client/src/VersionFooter.tsx create mode 100644 client/src/vite-env.d.ts diff --git a/client/src/VersionFooter.tsx b/client/src/VersionFooter.tsx new file mode 100644 index 0000000..594f576 --- /dev/null +++ b/client/src/VersionFooter.tsx @@ -0,0 +1,24 @@ +// #68: version/diagnostics display, shared by the two low-contrast +// footers (BarPicker, Stats) and Admin's fuller diagnostics block — +// same underlying info (build version + browser environment), different +// presentation for a quick glance vs. a bug-triage read. + +export const appVersion = __APP_VERSION__; + +export function VersionFooter() { + return ; +} + +// Read fresh on every call rather than once at module load — screen/ +// viewport size can change (rotation, window resize) between when the +// admin panel first mounts and when someone actually looks at this to +// triage a report. +export function diagLines(): string[] { + return [ + `Version: ${appVersion}`, + `User-Agent: ${navigator.userAgent}`, + `Bildschirm: ${screen.width}×${screen.height} @ ${window.devicePixelRatio}x`, + `Viewport: ${window.innerWidth}×${window.innerHeight}`, + `Sprache: ${navigator.language}`, + ]; +} diff --git a/client/src/admin/Admin.tsx b/client/src/admin/Admin.tsx index e3574bd..9dea9b9 100644 --- a/client/src/admin/Admin.tsx +++ b/client/src/admin/Admin.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from 'preact/hooks'; import type { Bar, Drink } from '@wutzcalc/shared'; import { errText, formatCents, j } from '../api'; +import { diagLines } from '../VersionFooter'; // `Drink`/`Bar` come from @wutzcalc/shared — the tablet code already did // this, but Admin.tsx used to re-declare its own near-identical copies, @@ -86,10 +87,25 @@ function Dashboard({ onLogout }: { onLogout: () => void }) { + ); } +// #68: browser/build metadata for triaging a device-specific bug report +// (e.g. "scrolling doesn't work on this one kiosk tablet") without having +// to walk someone through DevTools over the phone. +function Diagnose() { + return ( + <> +

Diagnose

+
    + {diagLines().map(l =>
  • {l}
  • )} +
+ + ); +} + function Exports() { return ( diff --git a/client/src/stats/Stats.tsx b/client/src/stats/Stats.tsx index 3adb6e8..d9e7483 100644 --- a/client/src/stats/Stats.tsx +++ b/client/src/stats/Stats.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState } from 'preact/hooks'; import { errText, formatCents, j } from '../api'; import { DayChart } from './DayChart'; +import { VersionFooter } from '../VersionFooter'; interface Totals { bar_id: number; bar_name: string; tx_count: number; paid_cents: number; crew_count: number; pfand_returns: number } interface PerDrink { drink_id: number; drink_name: string; sold_qty: number } @@ -257,6 +258,8 @@ function Dashboard({ data, isAdmin, onReset }: { data: StatsData; isAdmin: boole ))} + + ); } diff --git a/client/src/styles.css b/client/src/styles.css index f8a64f8..2565ac5 100644 --- a/client/src/styles.css +++ b/client/src/styles.css @@ -84,6 +84,21 @@ button.danger { background: #5a2a2a; border-color: #7a3a3a; color: #fff; } padding: 10px 18px; } +/* #68: version/build indicator for bug triage. margin-top: auto pins it + to the bottom of a flex column (.bar-picker) without disturbing that + column's own justify-content: center for everything above it; in a + plain block container (Stats.tsx's .admin) margin-top: auto has no + special effect, so it just falls in as the last item on the page — + same class works in both contexts without a variant. */ +.version-footer { + margin-top: auto; + text-align: center; + font-size: 12px; + font-weight: 600; + opacity: 0.35; + padding-top: 8px; +} + .topbar { display: flex; justify-content: space-between; @@ -311,6 +326,20 @@ button.danger { background: #5a2a2a; border-color: #7a3a3a; color: #fff; } } .admin .row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; } .admin .muted { opacity: 0.6; } +/* #68: readable-but-secondary — not as dim as .muted, this is meant to + actually be read for bug triage, just not competing with the primary + admin content above it. break-all because navigator.userAgent runs + long and would otherwise overflow .admin's max-width. */ +.diag-list { + list-style: none; + margin: 0; + padding: 0; + font-size: 13px; + font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; + opacity: 0.8; + word-break: break-all; +} +.diag-list li { padding: 3px 0; } .admin .login { max-width: 320px; margin: 80px auto; display: flex; flex-direction: column; gap: 12px; } .admin .chart { background: var(--surface); diff --git a/client/src/tablet/BarPicker.tsx b/client/src/tablet/BarPicker.tsx index fd81400..d2d4c83 100644 --- a/client/src/tablet/BarPicker.tsx +++ b/client/src/tablet/BarPicker.tsx @@ -1,6 +1,7 @@ import { useState } from 'preact/hooks'; import type { Bar } from '@wutzcalc/shared'; import { getTheme, setTheme } from '../api'; +import { VersionFooter } from '../VersionFooter'; interface Props { bars: Bar[] | null; @@ -35,6 +36,7 @@ export function BarPicker({ bars, onPick }: Props) { + ); } diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts new file mode 100644 index 0000000..57c1af3 --- /dev/null +++ b/client/src/vite-env.d.ts @@ -0,0 +1,5 @@ +/// + +// Injected by vite.config.ts's `define` — the short git commit sha the +// bundle was built from, or 'dev' if `.git` wasn't available at build time. +declare const __APP_VERSION__: string; diff --git a/client/vite.config.ts b/client/vite.config.ts index 8590f10..c8e32c3 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from 'vite'; import preact from '@preact/preset-vite'; import legacy from '@vitejs/plugin-legacy'; import { resolve } from 'node:path'; +import { execSync } from 'node:child_process'; // The dev-server proxy target — same default (3000) as the server's own // `process.env.PORT ?? 3000` in server/src/index.ts. Override with @@ -10,7 +11,23 @@ import { resolve } from 'node:path'; // WUTZ_SERVER_PORT=4000 pnpm dev:client const serverTarget = `http://localhost:${process.env.WUTZ_SERVER_PORT ?? 3000}`; +// #68: baked into the bundle at build time so the client can show "which +// build is this" for bug triage without a server round-trip. Deploys run +// `pnpm build` from a git checkout (see deploy/wutzcalc.service), but this +// stays defensive against a tarball/CI-artifact deploy with no `.git` — +// falls back to 'dev' rather than failing the build. +function appVersion(): string { + try { + return execSync('git rev-parse --short HEAD', { cwd: __dirname }).toString().trim(); + } catch { + return 'dev'; + } +} + export default defineConfig({ + define: { + __APP_VERSION__: JSON.stringify(appVersion()), + }, plugins: [ preact(), legacy({