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.
Closes#52, closes#55. Follow-ups from the #47 review round.
#52: by_hour_of_day was already zero-filled across 0-23 (deliberately, so a
quiet hour doesn't misread as missing data) but by_hour — the continuous
timeline — wasn't. The client renders it on a categorical axis, so closed
hours between two festival nights collapsed to nothing and the last hour of
one night sat directly next to the first hour of the next. Now zero-filled
between the first and last real bucket, same reasoning as by_hour_of_day.
#55, two fixes:
- DayChart's x scale is now explicitly ordinal (distr: 2). The prior default
(linear) let uPlot's tick generator pick fractional increments on a short
series, and the index-based label lookup misses on a non-integer tick,
rendering a blank label.
- The four per-day/per-hour chart data preps in Stats.tsx now produce a
single memoized {labels, series} object each, instead of building fresh
labels/series array literals inline in JSX on every render. DayChart's
effect is keyed on those props by reference, so the old code destroyed
and recreated every uPlot instance on any unrelated Dashboard re-render
(e.g. the isAdmin check resolving after data already loaded).
Both sides build/typecheck clean. Manually verified the zero-fill against a
seeded DB with a 3-hour gap between two transactions — by_hour correctly
returned 4 buckets (2 real, 2 zero-filled) in order.
Closes#40.
New /stats route (client/stats.html + src/stats/), served by the same
catch-all pattern as /admin. Reuses the admin login (STATS_PUBLIC env var
on the server side decides whether it needs one at all).
Three uPlot charts (daily revenue, daily transaction count, top-5-drink
sold-qty trend) plus the same three tables Admin.tsx used to render inline
— those move here wholesale, Admin.tsx now just links to /stats instead of
fetching /admin/api/stats itself. The 'Statistik zurücksetzen' reset button
moves here too, gated on an actual admin session (checked separately from
whether /api/stats itself succeeded, since STATS_PUBLIC can make that true
for an anonymous viewer).
Chart lib is uPlot (~45kb) per mara's steer not to hand-roll this. Both
client and server build/typecheck clean; manually smoke-tested the auth
gate (401 unauthed, 200 after login) and the /stats route against a fresh
DB.