dedupe j()/errText(), stale-fetch guard, empty bar-picker state
Six small, mostly-unrelated items from the #47 review round (#57):
1. 'Statistik zurücksetzen' now checks res.ok and shows the server's
error instead of silently re-rendering the un-reset data on a 401/500
with no indication anything went wrong.
2. DayChart now tracks its container width via ResizeObserver and calls
uPlot's setSize() on change — previously read once at plot-creation
time, so a resize/orientation-change (more visible on the phone/
tablet path into /stats) left the chart the wrong width.
3. .qty-btn (cart +/- buttons) grown from 28x28px to 44x44px, the
conventional minimum touch target — was the one conspicuously small
interactive control in the money path, at a fast-moving bar counter.
4. Tablet's bar-config fetch (App.tsx) now guards against an
out-of-order resolution if barId changes again while a previous
api.config() call is still in flight. Currently unreachable (nothing
triggers a second barId change mid-fetch today) but closes the gap
before the next 'add a cancel button to the loading screen' change
could make it live.
5. j()/errText() were duplicated byte-for-byte between Admin.tsx and
Stats.tsx (Admin.tsx's own comment noted this exact duplication had
already caused drift once before) — centralized in api.ts, both files
now import instead of redeclaring.
6. Tablet's bar picker now shows an explicit empty-state message when
zero bars exist (fresh install, or every bar deleted), previously
indistinguishable from 'still loading'.
Closes#57. Verified: tsc --noEmit and vite build both clean.
Follow-up to the #39 fix in d274275, which mara reports still doesn't
scroll in practice.
The bug: overflow-y: auto and justify-content: flex-end on the *same* flex
column is a known WebKit/Safari interop gap — the flex-end-justified
overflow gets clipped at the container bounds instead of becoming a
scrollable region, so nothing actually changed from the user's side despite
the CSS being 'correct' per spec. This app targets iOS Safari 12+
(vite.config.ts legacy targets), squarely in the affected range.
Fix: split the concerns onto two elements. .cart-items is now a plain
overflow-y: auto scroll container with no alignment property. The new
inner .cart-items-inner wrapper carries display:flex/flex-direction:column/
justify-content:flex-end, with min-height: 100% (a floor, not a fixed
height) so it still bottom-anchors a short list but grows past 100% and
scrolls normally in the outer container once content overflows it.
Build clean.
- 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.
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.
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.
- 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