Drinks and Bars each fetched + held their own copy of the drinks list. Adding
(or editing/archiving) a drink only updated Drinks's own copy — Bars's
BarDrinkEditor kept rendering the stale pre-change snapshot until a full page
reload re-mounted everything, so a newly added drink didn't show up in the
'add to bar' list without a manual refresh.
Lifted the drinks list + its reload fn into Dashboard, passed down as props to
both Drinks and Bars.
Fixes the reported /admin MIME-type / NS_ERROR_CORRUPTED_CONTENT breakage on the
Vite dev server. The proxy matched by string prefix, so '/admin' also caught
'/admin' and '/admin.html' (the page requests), forwarding them to the backend
instead of letting Vite serve its own dev-mode HTML. In dev the backend only has
a stale production build (or none) to answer with, so the page loaded referencing
hashed prod asset paths that don't exist in Vite's dev module graph — Vite's dev
server then served its SPA-fallback HTML for those asset requests instead of JS.
Narrowed the proxy to the three backend-owned endpoints under /admin
(api/login/logout) so the bare page routes go through Vite's own dev serving.
Admin.tsx defined its own local Drink/BarRow-adjacent interfaces
rather than importing from @wutzcalc/shared, even though the tablet
code in the same package already does (App.tsx, Sale.tsx,
BarPicker.tsx). The two had already drifted: the local Drink.archived
was typed number while the shared package's was boolean, despite
describing the exact same wire field. No runtime bug (JS doesn't
enforce it, and the code only does truthiness checks), but a
maintainability smell.
BarRow now extends the shared Bar type instead of duplicating its
fields; Drink is imported directly. Totals/PerDrink/ByDay stay local
since they're admin-only stats shapes, not part of the wire contract
the tablet also consumes.
- App.tsx: the error screen (shown when api.config(barId) fails, e.g.
an admin deleted/renamed the bar while this tablet was offline)
only offered 'Neu laden', which re-reads the same stale localStorage
bar_id and fails again — a permanent stuck loop with no in-app fix.
Added a 'Bar wechseln' button reusing the existing changeBar()
logic, which now also clears the error state.
- Admin.tsx BarDrinkEditor: the drag-and-drop reorder is a
mouse-oriented API that doesn't fire on touch-only input and has no
keyboard equivalent. Added ▲/▼ buttons alongside the drag handle so
reordering works regardless of input method.
- Drinks.add()/patch() now check res.ok and alert() the error text,
matching the pattern every other mutator in this file already
used (Drinks.del(), everything in Bars) — previously a rejected
name/price change just re-fetched the (unchanged) list with no
indication anything went wrong.
- Stats.reload(), Drinks.reload(), Bars.reload() now .catch() a
failed fetch/non-ok response instead of leaving an unhandled
promise rejection — a network hiccup used to leave the section
stuck on 'Lade…' forever with no visible error.
- Drinks 'Hinzufügen' and Bars 'Bar hinzufügen' are now disabled
while their POST is in flight, mirroring the submitting guard
Sale.tsx already uses — a fast double-tap could otherwise fire two
requests before the first reload() resolved.
The server dedupes on client_uuid, but the tablet minted a fresh uuid
on every confirm() call — including retries after a timeout/dropped
connection, exactly the case the idempotency key exists to guard
against. The dedup check never fired on a real retry, so a flaky-Wi-Fi
resend could book the same sale twice.
Client: generate one uuid per pending cart (a ref, lazily created),
reuse it across retries of the same submission, reset it only when
the cart is cleared (success or cancel) so the next cart gets its own
id.
Server: the existing-row dedup check and the insert straddled the
db.transaction() boundary, so a genuine UNIQUE-violation race would
have surfaced as a raw 500 instead of the idempotent response. Catch
that specific violation and fall back to re-reading the row.
vite.config.ts hardcoded the /api, /admin, /healthz proxy targets to
http://localhost:3000 — the server's default port — so there was no
way to run dev:client against a dev:server started on a different
port (PORT=<n> pnpm dev:server) without editing the config file.
Read the port from WUTZ_SERVER_PORT (same default of 3000 as the
server's own PORT env var) and build the proxy target once.
Cart entries (drink lines and Pfand-zurück) now have +/- buttons next
to the quantity instead of only accumulating via repeated taps;
decrementing to 0 removes the line. Long-pressing a drink tile opens
a small overlay with buttons 1-5 to add that many at once — a plain
tap still adds one. The long-press timer is cancelled on pointerup/
leave/cancel, and the click that follows a fired long-press is
swallowed so it doesn't also add a plain 1x.
On narrow phone screens the tablet UI had two problems:
- Drink button text could overflow the tile (no overflow: hidden / word-break),
causing garbled text spill visible in the issue screenshot.
- All font sizes and the cart height were tablet-sized (22px drinks, 56px total,
38vh cart, 64px action buttons); no phone overrides existed beyond the 2-column
grid switch.
Fixes:
- .drink: add overflow: hidden so text never bleeds outside the tile.
- .drink .name: word-break + overflow-wrap so long names wrap rather than clip.
- .topbar .bar-name: nowrap + text-overflow: ellipsis so a long bar name
truncates cleanly rather than wrapping/overflowing.
- @media (max-width: 480px): scale down topbar (20px), drink tiles (17px),
pfand label (12px), cart height (34vh), cart total (38px), action buttons
(52px / 18px) to fit a portrait phone comfortably.
Closes#3.
User feedback (#1): drink buttons all the same size in a 3-column grid with
any number of rows; scroll when more than ~5 rows of drinks exist; entry
overview + sum stay visible. Device: iPad mini 2 (portrait).
The grid used `grid-auto-rows: 1fr` + `overflow: hidden`, which divided the
available height across however many drinks there were — tiles shrank as the
catalog grew and it never scrolled (the known UX/scaling item in TODO.md).
Now: `grid-auto-rows: calc((100% - 4 * var(--gap)) / 5)` — the row height is
DERIVED so exactly five equal rows fill the visible grid area (no hardcoded
tile pixel size), with `align-content: start` + `overflow-y: auto` (+ iOS
momentum scroll). Tiles are a consistent size regardless of count, ~5 rows
show, the rest scroll. The cart stays pinned below (unchanged) so the overview
+ sum remain permanently visible.
CSS-only.
- admin: maroon buttons use .danger class (white text); replace hardcoded
#333/#ff8a8a/drop-target colors with theme vars so light mode is legible
- time.ts: force hourCycle h23 (no 24:00 artifact), drop dead 24-guard
- Makefile/README: rsync with excludes instead of `cp -a .` so .git and the
dev DB don't ship to /opt; add rsync to deps
- favicon.svg (beer mug), linked from both entry points and shown in
the tablet topbar next to the bar name
- inline data-theme in <head> kills the flash of dark on light-mode load
- hide the "Pfand zurück" tile when the bar has no Pfand
- show drink price and Pfand separately on tiles and in cart
- add light/dark toggle on the bar-picker page (persisted)
- theme via CSS variables, applied on load for tablet + admin
- larger, bolder text throughout; much bigger total sum
- store created_at as UTC ISO; display/group in Europe/Berlin
- stats grouped by business day (sales night past midnight, 5am cutoff)
- add "Statistik zurücksetzen"
- allow deleting drinks/tresen (refused if referenced by sales)
- CSV exports local wall-clock time