getUserMedia (and the awaited play()) could hang indefinitely on iOS when the
scanner mounts after navigation (not in a user-gesture context), leaving the
UI stuck with no recovery. Add a 12s watchdog that surfaces an error + Retry
button (Retry is a fresh gesture iOS honors), stop awaiting play() (fire and
forget), and stop any prior stream before re-requesting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI: limit Gradle to 2 worker JVMs (org.gradle.workers.max=2, --max-workers=2,
parallel=false), cap native compiler jobs (CMAKE_BUILD_PARALLEL_LEVEL=2), and
build only the arm64-v8a ABI — so the build no longer spawns a JVM/compiler per
core and swamps the host (which also serves Forgejo).
App: the success chime now fires the instant a scan resolves to a valid ticket
(ticket/ice modes), matching Banquet, instead of after the check-in round-trip.
The failure sound is unchanged. Removed the duplicate chime at check-in.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the two-note chirp with a bell-like C–E–G–C arpeggio (soft attack,
gentle decay, light harmonics) for a more pleasing check-in confirmation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Login: fixed-length 4-digit PIN that auto-submits on the 4th digit (no submit
button to scroll to on small iPhone screens) and clears on a wrong PIN.
Compact, vertically-centered keypad so it fits without scrolling.
Operator tracking: after PIN auth, staff enter their name (new /operator
screen, persisted per device). The name is sent as X-Operator on every authed
request and recorded on each check-in/undo/ice audit entry (new Operator
column), so logs show who did what. Shown in the scanner header and the admin
audit view.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The web scanner's camera loop starts once in useEffect and closed over the
mount-time onScan handler, so switching modes (e.g. to Banquet) kept invoking
the original ticket-check-in handler. Route onScan through a ref updated each
render so the loop always calls the current handler. Native was unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Auth: introduce a shared AuthProvider/useAuth so entering the PIN updates
reactive state and the layout's gate navigates immediately (previously it
cached the token check at startup, so login appeared to hang until a manual
refresh). login/scanner now use signIn/signOut.
- Banquet: donor lookup now returns lifetime giving, last-12-months giving
(computed from dated transactions), member/donor status, bear name, and tags.
The banquet result screen shows status badge + lifetime and last-year figures.
Tickets are irrelevant in banquet mode.
- Admin: fix "‹ Scanner" back link wrapping (remove fixed width).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
react resolved to 19.2.7 (via react-native) while react-dom was pinned to
19.2.3, causing React error #527 (version mismatch) and a blank page on web.
Pin both to 19.2.7. Native APK was unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Ice mode: prepaid ice bags (Ice Total / Ice Redeemed columns) redeemed
independently of ticket check-ins; grab all bags at once or some now.
- Banquet mode: donor total (online + offline) looked up by the ticket's
email via the Donors Master List, with a manual email override. New
DonorService + POST /api/banquet.
- Redeem generalized over a resource (tickets|ice); audit records ice actions.
- App gains a mode selector; webhook maps ice_bags (defaults to
ICE_BAGS_DEFAULT when only a boolean ice option is present).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every successful check-in/undo writes a row to the "2026 Ticket Audit Logs"
NocoDB table (timestamp, people, code, action, name, remaining-after).
Non-fatal: audit failures never block a gate check-in. New GET /api/audit
endpoint (global or per-code). Admin panel gains a global "Recent check-ins"
panel and per-ticket history. Audit table is optional via NOCODB_AUDIT_TABLE_ID.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>