New form schema: up to 10 named adults, youth 13-16, kids 10-12 / 5-9 / 0-4,
donor tier/vouchers (from the lookups), parking/RV/UTV/ice payment fields.
- Scannable total = adults + youth + kids 10-12 + kids 5-9 (kids 0-4 free).
- Store + display adult names on a good scan; show donor tier, UTV, vouchers.
- Ice: payment_ice = 1-4 tickets ($20 each), 1 ticket = 3 bags.
- New NocoDB 2026 schema (with Id PK); webhook parses compound names,
quantity/payment fields (nested objects or money strings).
- Our webhook keeps sending the QR ticket email (FluentForms sends the receipt);
from address is now info@beartariacampgrounds.com.
Updated /test personas, /webhook-doc, tests, and the app display.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the ticket-voucher API into its own standalone doc
(docs/fluentforms-ticket-vouchers.md) — endpoint, 0/1/2 rules, config, form
snippet, and curl test — so it's easy to find and hand off for the lookup. The
donor-discount doc now links to it instead of duplicating the section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /api/public/ticket-vouchers?key=&email= returns 0/1/2 free tickets based on
donations on/after VOUCHER_SINCE (default 2025-09-04): >= $400 -> 1, >= $1000 -> 2.
Same secret/CORS/rate-limit as donor-eligibility; returns only the count. Cutoff
and thresholds are env-configurable. Documented both lookup APIs (discount +
vouchers) in docs/fluentforms-donor-discount.md with form snippets.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A green icon where a QR code's finder squares form a bear face — brown ears on
top, cream snout in the center. Wired as the main icon, the Android adaptive
icon (foreground + #0f1a12 background), and the web/PWA icons + favicon.
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>
Critical: NocoDB v2 PATCH /records with no primary key updates EVERY row, so a
table missing its Id column made each redeem rewrite all tickets' counts.
- nocodb update() now refuses to PATCH a record with no Id (fail-safe).
- Document that the tickets/audit tables must have an Id PK; note how to add it.
- Replace the real donor email/name used in the /test persona and the
FluentForms guide with fake placeholders (donor@example.test / <a-real-donor-email>).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hidden donor_tier field (regular/donor/member) driven by the email lookup;
payment options shown via FF conditional logic. Uses the native value setter +
input/change dispatch so FF's Vue model registers the programmatic change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /api/public/donor-eligibility?key=&email= returns only {eligible, tier}
(member/donor) — no names or dollar amounts — gated by PUBLIC_LOOKUP_SECRET,
rate-limited (30/min), and CORS-restricted to PUBLIC_LOOKUP_ORIGIN. Lets the
FluentForms checkout unlock a donor discount by email. Docs + ready-to-paste
form snippet in docs/fluentforms-donor-discount.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Served at /webhook-doc: endpoint + auth header, full field table (name/email,
age brackets, ice, parking, donor, idempotency key), example JSON + curl,
response codes, and FluentForms feed setup steps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Served unauthenticated at /install: one-tap "Add to Obtainium" deep link,
how to get Obtainium, direct-APK fallback to the Forgejo releases, and iPhone
Add-to-Home-Screen steps. Detects the visitor's platform and shows it first.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The build failed with "Archive is not a ZIP archive" when AGP auto-downloaded
NDK 27.1.12297006 mid-build (truncated archive). Install the NDK and CMake up
front via sdkmanager (checksum-verified, robust) so AGP finds them already
present, and wipe any incomplete ndk/cmake dir (missing source.properties) left
in the cache volume by a failed run so it reinstalls cleanly.
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>
The runner host (~20GB/4-core, shared with Forgejo + databases) OOM-thrashed
because the RN build spawned ~3 Kotlin daemons at -Xmx5376m each. org.gradle.jvmargs
only bounds the Gradle JVM, not the forked Kotlin daemons — so write a
gradle.properties to GRADLE_USER_HOME with kotlin.daemon.jvmargs=-Xmx1536m,
org.gradle.jvmargs=-Xmx1536m, workers.max=2, parallel=false. Combined with the
earlier single-ABI + CMAKE_BUILD_PARALLEL_LEVEL=2 caps, the build no longer
swamps the host.
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>
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>
Persist three Docker volumes into the build job (android-sdk, gradle home, npm
cache) and enable the Gradle build cache, so repeat builds skip the ~2GB NDK
download, Maven dependency resolution, and unchanged native/Kotlin compilation
— cutting builds from ~1h to a few minutes after the first run.
The runner must whitelist these volumes via config.yaml (container.valid_volumes);
deploy-runner.sh now writes that config, pre-creates the volumes, and starts the
daemon with --config. Requires re-running deploy-runner.sh on the runner host
before the next tag.
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>
The runner host has no working IPv6 route; Maven Central (AAAA records) was
'Network is unreachable' while IPv4-only CDNs worked. Set
JAVA_TOOL_OPTIONS=-Djava.net.preferIPv4Stack=true so all build JVMs use IPv4.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /test (gated by ENABLE_TEST_PAGE) renders scannable QR codes for a set of
personas covering different attributes — solo, family with ice+parking, a real
donor for Banquet mode, ice-only, a pre-exhausted ticket, and an invalid code.
Idempotently seeds them into the current NocoDB table.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
scripts/deploy-runner.sh installs Docker if missing, writes the runner
compose + env, registers against git.mowden.top, and starts the runner —
one command on the roomy server.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- gen-keystore.sh now builds a PKCS12 keystore with openssl (no JDK/keytool),
and signing.gradle declares storeType PKCS12.
- Runner is meant to run on a roomy server (the app host lacks disk for the
Android SDK); added runner/README.md with deployment steps. Removed the
runner that was registered on the app host.
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>