# πŸ—ΊοΈ wutzcalc β€” Implementation Plan Festival drink-sale tracker. 3 bars, ~10k attendees over 1 week, old iPads (iOS 12), local network only, central linux server. ## πŸ”’ Decisions (locked for v1) - **Stack:** Preact + Vite (client), Node + Fastify (server), SQLite (better-sqlite3). - **Deployment:** single Node process serves API + static client + opens one `.db` file. - **Sync model:** online-only. Confirm requires server reachable on LAN. - **Auth:** none on tablets in v1 (trusted LAN); `/admin` gated by a shared password env var. - **Tablet identity:** server records source IP per transaction (`client_ip`) to attribute transactions to a tablet. Assumes static DHCP leases or stable IPs on the festival LAN. - **Drink catalog:** shared across bars; each bar picks which drinks it serves. - **Pfand:** single global pfand value per bar; added on top of drink price. - **PfandrΓΌckgabe:** dedicated minus/return buttons per drink; endbetrag may go negative. - **Crew/freebies:** a separate "BestΓ€tigen (Crew)" confirm button β€” logs transaction with `crew=true`, paid amount = 0, items still recorded. - **Undo:** only before confirming, fully local (edit the current cart). No undo of confirmed transactions in v1. - **iOS 12 target:** Vite build with `legacy` plugin β†’ ES2017 + polyfills. No CSS features past Safari 12. ## πŸ—οΈ Architecture ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” HTTP/JSON β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Tablet (PWA) β”‚ ───────────────► β”‚ Node + Fastify β”‚ β”‚ Preact SPA β”‚ β”‚ /api/... β”‚ β”‚ per-bar UI β”‚ β”‚ /admin (pw-gated) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ static client assets β”‚ β”‚ ───────────────────── β”‚ β”‚ better-sqlite3 ── wutz.db β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` - Single port, single binary-ish (`node server.js`). Systemd unit for autostart. - SQLite WAL mode for concurrent reads from dashboard while writes happen. ## πŸ—„οΈ Data model (SQLite) ``` bars(id, name, pfand_cents) drinks(id, name, price_cents, archived) bar_drinks(bar_id, drink_id, sort_order) -- which drinks each bar sells transactions( id, bar_id, created_at, total_cents, crew INTEGER NOT NULL DEFAULT 0, client_ip TEXT, -- which tablet (per-bar device id) client_uuid TEXT UNIQUE -- idempotency from tablet ) transaction_items( id, transaction_id, drink_id, qty, unit_price_cents, pfand_cents_per_unit, is_return INTEGER NOT NULL DEFAULT 0 -- pfandrΓΌckgabe line ) settings(key, value) -- admin password hash etc. ``` - All money in integer cents. - `client_uuid` makes confirm idempotent against double-tap / retry. - Returns stored as separate line items with `is_return=1` and negative contribution to total. ## πŸ”Œ API surface - `GET /api/config?bar=` β†’ bar + its drinks + pfand value. - `GET /api/bars` β†’ list of bars (for tablet first-run picker). - `POST /api/transactions` β†’ body: `{ client_uuid, bar_id, crew, items: [{drink_id, qty, is_return}] }`. Server recomputes prices from current catalog, captures source IP (`req.ip`, trust proxy off) as `client_ip`, stores, returns `{id, total_cents}`. - `GET /admin` β†’ password form β†’ session cookie. - `GET /admin/api/stats` β†’ totals per bar, per drink, crew vs paid. - `GET /admin/api/export.csv?what=transactions|items` β†’ CSV download. - `POST /admin/api/drinks` / `PATCH` / `DELETE` β†’ catalog config. - `POST /admin/api/bars/:id` β†’ pfand + drink selection. Server is the source of truth for prices β€” tablet sends drink IDs + qty only. ## πŸ“± Client (tablet) UX - **First run:** pick which bar this tablet is. Stored in `localStorage`. - **Main screen:** grid of large buttons, one per drink served at this bar. - Tap β†’ adds 1 to cart. - Long-press / "βˆ’" sub-button β†’ returns 1 pfand for that drink. - **Cart strip:** running total (price + pfand βˆ’ returns), per-line qty. - **Actions:** `Abbrechen` (clear cart), `BestΓ€tigen` (send, crew=false), `BestΓ€tigen (Crew)` (send, crew=true, paid=0). - All cart edits are local until confirm β€” undo = remove from cart. - Tap targets β‰₯ 64px, no hover states, no small text. ## 🏒 Backoffice (v1) - `/admin` login (single password from env `ADMIN_PASSWORD`). - Pages: 1. **Drinks**: CRUD table (name, price, archived). 2. **Bars**: per bar β€” pfand value + checklist of drinks served + sort order. 3. **Stats**: totals per bar, top drinks, crew freebie count/value. 4. **Export**: CSV of transactions and transaction_items. ## πŸ“ Repo layout ``` wutzcalc/ β”œβ”€β”€ server/ β”‚ β”œβ”€β”€ src/ β”‚ β”‚ β”œβ”€β”€ index.ts # fastify bootstrap, static serving β”‚ β”‚ β”œβ”€β”€ db.ts # better-sqlite3 + migrations β”‚ β”‚ β”œβ”€β”€ routes/ β”‚ β”‚ β”‚ β”œβ”€β”€ public.ts # /api/* (tablets) β”‚ β”‚ β”‚ └── admin.ts # /admin/* β”‚ β”‚ └── csv.ts β”‚ └── migrations/ β”œβ”€β”€ client/ # Preact + Vite β”‚ β”œβ”€β”€ src/ β”‚ β”‚ β”œβ”€β”€ main.tsx β”‚ β”‚ β”œβ”€β”€ pages/{Bar,Setup}.tsx β”‚ β”‚ β”œβ”€β”€ components/{DrinkButton,Cart}.tsx β”‚ β”‚ └── api.ts β”‚ └── vite.config.ts # @vitejs/plugin-legacy β†’ iOS 12 target β”œβ”€β”€ admin/ # could share Preact app or be plain HTML forms β”œβ”€β”€ package.json # workspaces β”œβ”€β”€ README.md β”œβ”€β”€ NOTES.md β”œβ”€β”€ TODO.md └── PLAN.md ``` (Admin can be a second entry in the same Vite build, or server-rendered HTML β€” leaning toward second Vite entry for consistency.) ## πŸ”’ Implementation order 1. Repo scaffold: pnpm workspaces, Fastify server skeleton, Vite client skeleton, shared types package. 2. SQLite schema + migrations + seed (3 bars, a few drinks). 3. `/api/config` + `/api/transactions` with idempotency on `client_uuid`. 4. Tablet UI: bar picker β†’ drink grid β†’ cart β†’ confirm/cancel/crew-confirm. 5. Admin password gate + drinks/bars config UI. 6. Stats endpoint + minimal dashboard. 7. CSV export. 8. Legacy build verification on an iOS 12 Safari (or BrowserStack equivalent). 9. Systemd unit + README deploy instructions + backup cron note. ## ⚠️ Open risks - iOS 12 Safari is the main constraint β€” need to verify Preact + legacy plugin output actually runs there early (after step 1, before building more). - Local network reliability: if the WiFi at the venue is flaky, online-only model bites. Tracked in `TODO.md` (offline-capable client). - SQLite single-writer is fine at ~10k attendees / week, but document WAL mode and a backup cron.