No description
  • TypeScript 86%
  • CSS 8.5%
  • Makefile 2.6%
  • HTML 1.9%
  • Nix 1%
Find a file
iris 5f0a7bf6cc server: RFC7807 404s without a client build; document STATS_PUBLIC
Two of the three items from #54 (item 1, the dead is_return column, is
a drop-or-use product call left for mara — see issue comment):

1. The RFC7807 404 handler for /api and /admin/api was only registered
   inside the 'if (existsSync(clientDist))' branch, so a server-only
   deploy (or any run before 'pnpm --filter client build') fell through
   to Fastify's default 404 shape instead — quietly opting API routes
   out of the one-error-shape guarantee the RFC7807 conversion was for.
   Split registration from the static-file serving: the not-found
   handler (and its API-vs-SPA-fallback branching) is now unconditional,
   only the SPA html sendFile calls stay gated on hasClientDist.
2. STATS_PUBLIC added to the README's env-var list (was only in
   deploy/wutzcalc.env.example) — an operator wanting the infopoint-
   screen setup wouldn't find the switch, and an operator auditing
   "what can expose data here" from the README wouldn't see it exists.

Verified live: renamed client/dist away and confirmed /api, /admin/api,
and a bare unmatched route all return RFC7807 problem+json (no crash);
restored it and confirmed the SPA fallback (admin.html/stats.html/
index.html) still serves correctly. tsc --noEmit and server build both
clean.
2026-07-31 12:52:29 +02:00
client client: fix reset-error handling, undersized qty buttons, chart resize, 2026-07-31 12:52:01 +02:00
deploy server: /api/stats endpoint + per-day-per-drink trend data 2026-07-30 11:40:34 +02:00
server server: RFC7807 404s without a client build; document STATS_PUBLIC 2026-07-31 12:52:29 +02:00
shared backend polish: stats LEFT JOIN, CSV formula injection, trustProxy, validated env vars, dead code 2026-07-29 20:56:32 +02:00
.gitignore gitignore: drop local claude settings 2026-05-19 18:23:03 +02:00
flake.lock tablet: fit grid and cart on screen, no scrolling 2026-05-19 18:14:14 +02:00
flake.nix add repo url to readme, package.json, flake 2026-05-19 18:39:52 +02:00
Makefile review fixes: light-mode contrast, tz robustness, deploy copy 2026-06-14 22:55:29 +02:00
NOTES.md docs: logo, security warning, emoji headings 2026-06-14 23:17:56 +02:00
package.json add repo url to readme, package.json, flake 2026-05-19 18:39:52 +02:00
PLAN.md docs: logo, security warning, emoji headings 2026-06-14 23:17:56 +02:00
pnpm-lock.yaml client: standalone /stats page with charts, off the admin dashboard 2026-07-30 11:40:34 +02:00
pnpm-workspace.yaml scaffold festival drink tracker (pnpm workspace, Fastify + SQLite, Preact tablet UI, admin) 2026-05-19 18:12:01 +02:00
README.md server: RFC7807 404s without a client build; document STATS_PUBLIC 2026-07-31 12:52:29 +02:00
TODO.md docs: logo, security warning, emoji headings 2026-06-14 23:17:56 +02:00
tsconfig.base.json scaffold festival drink tracker (pnpm workspace, Fastify + SQLite, Preact tablet UI, admin) 2026-05-19 18:12:01 +02:00

wutzcalc 🍺

wutzcalc logo

⚠️ AI-generated code

Every file in this repository was written by an AI coding assistant. It has not been independently audited or reviewed line-by-line by a human. Before running this in any setting where correctness matters (real money, real events), read the code yourself, test the edge cases that matter to you, and assume there are bugs. Use at your own risk.

🔓 No security — trusted networks only

This app has essentially no protection. Assume anyone who can reach it can read and write everything.

  • 🚫 The tablet UI (/, /api) has no authentication — anyone on the network can record sales, return Pfand, or reset a cart.
  • 🔑 The backoffice (/admin) is gated by a single shared password in plaintext (ADMIN_PASSWORD), sent and stored as-is — no per-user accounts, no rate limiting, no audit log.
  • 🌐 There is no HTTPS built in — traffic (including the admin password) is plaintext unless you put it behind your own TLS-terminating reverse proxy.
  • 🧱 No CSRF tokens (only a SameSite=Lax cookie) and no input hardening beyond basic request validation.

Run it only on a trusted, isolated LAN (e.g. the bar's own Wi-Fi/VLAN), never exposed to the public internet. See TODO.md for the auth and hardening work deferred from v1.

🎪 Festival drink-sale tracker. See PLAN.md for architecture, NOTES.md for the original requirements, and TODO.md for deferred work.

🔗 Source: https://git.berlin.ccc.de/vinzenz/wutzcalc

🔧 Toolchain setup

Requires Node.js 20+ and pnpm 9+. Native SQLite bindings install from prebuilt binaries on x86_64 / arm64 — no compiler needed for most setups.

❄️ Nix (Linux / macOS)

nix develop          # node 20, pnpm, sqlite, build deps

🐧 Debian / Ubuntu

# Node 20 from NodeSource
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
sudo corepack enable             # provides pnpm

# Only needed if a prebuilt better-sqlite3 binary is unavailable for your arch
sudo apt install -y build-essential python3

🎩 Fedora

# Node 20 + pnpm (corepack ships with the nodejs package)
sudo dnf install -y nodejs
sudo corepack enable             # provides pnpm

# Only needed if a prebuilt better-sqlite3 binary is unavailable for your arch
sudo dnf install -y gcc-c++ make python3

🪟 Windows

Install Node.js 20 LTS via the official MSI from https://nodejs.org (this also installs the optional "Tools for Native Modules"). Then in PowerShell:

corepack enable                  # provides pnpm

Use PowerShell to set env vars on the same line, e.g. $env:ADMIN_PASSWORD="changeme"; pnpm dev:server.

🛠️ Dev

pnpm install
ADMIN_PASSWORD=changeme pnpm dev:server   # http://localhost:3000
pnpm dev:client                            # http://localhost:5173 (proxies /api, /admin)

Open http://localhost:5173/ for the tablet UI and http://localhost:5173/admin.html for the backoffice.

Running dev:server on a non-default PORT? Point dev:client's proxy at it with WUTZ_SERVER_PORT (same value as PORT):

PORT=4000 ADMIN_PASSWORD=changeme pnpm dev:server
WUTZ_SERVER_PORT=4000 pnpm dev:client

📦 Production build

pnpm install
pnpm build
ADMIN_PASSWORD=... DB_PATH=/var/lib/wutzcalc/wutz.db node server/dist/index.js

Single Node process serves the API, both client entries (/ tablet, /admin backoffice), and writes to one SQLite file.

🚀 Run as a systemd service

On Fedora, the Makefile automates everything below — from a fresh checkout, as root:

sudo make install                      # system deps + build + service
sudoedit /etc/wutzcalc/wutzcalc.env    # set ADMIN_PASSWORD
sudo systemctl enable --now wutzcalc

Override paths with e.g. make install PREFIX=/srv/wutzcalc SERVICE_USER=wutz.

The manual steps below do the same thing. Template files live in deploy/: a unit (wutzcalc.service) and an environment file (wutzcalc.env.example). They assume the built app lives in /opt/wutzcalc and the database in /var/lib/wutzcalc — adjust paths in the unit if yours differ.

# 1. Dedicated system user (no login, no home)
sudo useradd --system --no-create-home --shell /usr/sbin/nologin wutzcalc

# 2. Install the built app (run `pnpm install && pnpm build` first)
sudo mkdir -p /opt/wutzcalc
sudo rsync -a --exclude='.git' --exclude='*.db*' ./ /opt/wutzcalc/
sudo chown -R root:root /opt/wutzcalc # app dir stays read-only to the service

# 3. Config + secrets (chmod 600 — holds ADMIN_PASSWORD)
sudo mkdir -p /etc/wutzcalc
sudo install -m 600 deploy/wutzcalc.env.example /etc/wutzcalc/wutzcalc.env
sudoedit /etc/wutzcalc/wutzcalc.env    # set ADMIN_PASSWORD

# 4. Install and start the unit
sudo cp deploy/wutzcalc.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now wutzcalc

# Logs / status
systemctl status wutzcalc
journalctl -u wutzcalc -f

Confirm ExecStart matches your Node path (command -v node) — it defaults to /usr/bin/node. The unit creates /var/lib/wutzcalc via StateDirectory, so the service user owns the database directory automatically.

⚙️ Env vars

  • PORT (default 3000)
  • HOST (default 0.0.0.0)
  • DB_PATH (default ./wutz.db)
  • ADMIN_PASSWORD (required for backoffice login)
  • WUTZ_TZ (default Europe/Berlin) — timezone for stats display and grouping
  • WUTZ_DAY_CUTOFF_HOUR (default 5) — sales before this local hour count toward the previous business day
  • WUTZ_TRUST_PROXY (default off, set to 1 to enable) — trust X-Forwarded-* headers for req.ip. Only turn this on if you're actually running behind a reverse proxy (see the systemd/deploy section) — otherwise a client can spoof its own logged IP.
  • WUTZ_SERVER_PORT (default 3000, client dev only — not read by the server) — the port dev:client's Vite proxy targets; set it to match dev:server's PORT when running the server on something other than the default
  • STATS_PUBLIC (default off, set to 1 to enable) — makes /stats viewable without the admin login normally required (e.g. a screen permanently mounted at a festival infopoint). Doesn't affect /admin itself, or the "Statistik zurücksetzen" reset button, which stays gated on a real admin session either way.