wutzcalc/README.md
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

176 lines
6.5 KiB
Markdown

# wutzcalc 🍺
<img src="client/public/favicon.svg" alt="wutzcalc logo" width="120" align="right" />
> ## ⚠️ 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](TODO.md) for the auth and
> hardening work deferred from v1.
🎪 Festival drink-sale tracker. See [PLAN.md](PLAN.md) for architecture, [NOTES.md](NOTES.md)
for the original requirements, and [TODO.md](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)
```sh
nix develop # node 20, pnpm, sqlite, build deps
```
### 🐧 Debian / Ubuntu
```sh
# 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
```sh
# 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:
```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
```sh
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`):
```sh
PORT=4000 ADMIN_PASSWORD=changeme pnpm dev:server
WUTZ_SERVER_PORT=4000 pnpm dev:client
```
## 📦 Production build
```sh
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:
```sh
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/`](deploy/): a unit ([`wutzcalc.service`](deploy/wutzcalc.service)) and
an environment file ([`wutzcalc.env.example`](deploy/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.
```sh
# 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.