scaffold festival drink tracker (pnpm workspace, Fastify + SQLite, Preact tablet UI, admin)
This commit is contained in:
parent
cf33e6ea04
commit
e0898bea22
34 changed files with 5446 additions and 0 deletions
138
PLAN.md
Normal file
138
PLAN.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue