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
wutzcalc {appVersion}
;
+}
+
+// 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({