The admin area belongs at /crush33 (the standalone, portal-password
page — no app login), not an in-app /comp route I'd added unasked.
- Rebuilt the /crush33 page into the full hub: password unlock →
sidebar (Comp tickets · Donor lookup · Actions). Vanilla JS calling
the same /api/portal + /api/admin endpoints. Actions has the danger
cards + an "are you sure" modal spelling out exactly what happens.
- Deleted app/app/comp.tsx (removes the /comp route).
- Drawer "Admin (crush33)" now opens the /crush33 web page (Linking)
instead of routing to /comp.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuilt the password-gated /crush33 (in-app /comp) screen into an admin
hub with a left sidebar and three sections:
- Comp tickets — the existing entry-only comp creator.
- Donor lookup — admin-only free-text search across the donor master
list + online/offline transaction tables by name / email / phone /
address / bear name (columns discovered per table, deduped by email).
- Actions (danger zone) — heavy warnings, red buttons, and an
"are you sure" modal that spells out exactly what will happen:
• Wipe slate — delete ALL ticket + audit records in the active
event table (donor data untouched, irreversible).
• Switch event table — repoint the app at a different NocoDB
tickets/audit table to start a new event while keeping the old
one intact.
Backend:
- New /api/admin/{status,wipe,switch-table,donor-search}, all gated by
PORTAL_PASSWORD (POST-only so it never lands in a URL/log).
- NocoDBClient + AuditLogger: runtime-switchable tableId, count(),
deleteAll(), probeTable() (reachable + Id-PK check before switching).
- DonorService.search() with adaptive column discovery.
- Table switch persists across redeploys via a small state file on a
new /data volume (Dockerfile creates it owned by node so it's
writable); applied at startup in buildContext.
Also shipped equivalent CLI scripts: scripts/wipe-slate.sh and
scripts/switch-event.sh. Drawer: "Comp tickets" -> "Admin (crush33)".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Reporting: GET /api/stats aggregates check-in progress, ice, ticket types,
people breakdown, add-ons/donors, gate-crew leaderboard (from audit),
comp tickets by creator, and a by-hour check-in timeline. New /stats screen.
- Slide-out drawer (custom RN Animated, no new native deps) replaces per-screen
header links; available on every main screen via a hamburger.
- In-app comp portal (/comp), password-gated like /crush33, reusing the portal
endpoints; records the issuing gate-staff name (Created By column) and reports
comps per creator.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Move the banquet manual-email input to the top of the scanner (under the mode
tabs) so the on-screen keyboard, which covers the bottom, never hides it.
- Restrict QR detection to the centered reticle square: native filters codes by
their reported position (fails open if geometry is unavailable); web crops the
central square of the frame before detecting. Codes elsewhere in view are
ignored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Auto-start on mount as before, but keep a tappable overlay until the video is
actually playing (readyState>=2). If getUserMedia hangs (8s watchdog), is
denied, or fails, the overlay becomes a "tap to start camera" button that
retries in a real user-gesture context — instead of sitting on "Starting
camera…" with no recovery. Works for both Android Chrome PWA and iOS Safari.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>