wutzcalc/PLAN.md

6.8 KiB
Raw Blame History

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=<id> → 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.