Commit graph

41 commits

Author SHA1 Message Date
62231e9b10 Release v0.2.0 (versionCode 2)
Webhook: Tickets 2026 form (customer_name title, voucher-name ticket
counting, ticketless ice/UTV orders, donor adult names); ice bag count
fix; voucher decrement; vendor webhooks; free kids through 12; multi-
origin lookup CORS; scanner Adults/Kids party panel + ice default 1.
First versionCode bump (was stuck at 1), so this installs over v0.1.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 00:34:32 +00:00
63e00fae61 Scanner: large Adults / Kids party panel on scan
Gate-enforcement aid against adults signing up under a free/cheaper kid
bracket. When a ticket is scanned, a prominent amber-bordered panel
shows big "# ADULTS  #  KIDS" counts (Adults = the 18+ bracket; Kids =
youth 13-16 + all under-13), plus a per-bracket detail line (e.g.
"1× 13-16 · 2× 5-9") so staff can eyeball the claimed ages against the
actual party. Shown on the confirm card (before check-in) and the
success overlay; hidden in Ice mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:48:10 +00:00
049f433930 Fix ice bag count + default ice check-in to 1 bag
The form sends payment_ice as a descriptive label, e.g. "One Ice
ticket good for one bag per day (3 total bags)". The old parser pulled
the first number ("3") and treated it as 3 tickets, then multiplied by
3 bags/ticket → 9 bags for one ice ticket (18 for two). New
iceBagsFromPayment reads the "(N total bags)" the label states
directly, with worded-count and numeric dollar/count fallbacks for
forward compatibility. 1 ice → 3 bags, 2 → 6. 5 new tests.

Scanner: Ice mode now defaults the check-in count to 1 (a bag at a
time) instead of all remaining bags; staff can bump it up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 07:40:05 +00:00
49e45ff94c Webhook: count voucher tickets + use customer_name as title
Two corrections to the adult-ticket model:

1. Adult total was undercounting. Voucher (donor) tickets live only in
   the names_Donor_1 / names_Donor_2 name fields — each filled name is
   one free voucher adult ticket — and weren't counted at all. Adults
   now = item_quantity_adult_ticket_reg (regular) +
   item_quantity_adult_ticket_donor (extra PAID donor tickets beyond
   vouchers) + the donor voucher-name count. Vouchers consumed is now
   that same donor-name count (what the ticket-voucher lookup subtracts),
   instead of the hidden `vouchers` entitlement.

2. Ticket title now comes from customer_name (billing name), not the
   first adult ticket name.

Doc updated to describe the adult total and the title source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 19:07:56 +00:00
a987e046da Webhook: customer_name purchaser + allow ticketless orders
Two fixes for the updated Tickets 2026 form:

1. Purchaser name now comes from the new customer_name (billing) field,
   falling back to the first attendee then a plain `name`. Donor-only
   and buy-for-others orders (where the Adult #1 `names` group is empty)
   no longer 400 with "purchaser name is required". The ticket title is
   the first attendee if present, else the customer; the QR email is
   addressed to the customer.

2. Tickets are now optional. A customer can buy ice / ATV-UTV / parking
   with no admission ticket. A record + QR is created whenever there's
   anything to redeem or verify at the gate (ticket, ice, or add-on);
   only a truly empty order is rejected (no_items, replacing no_tickets).

The ticket email adapts its copy for ticketless (add-on-only) orders —
it reads as a gate pass for ice/parking/UTV instead of "0 tickets", and
names the ice bag count when present. Idempotency hash now includes
ice/extras so distinct add-on-only orders don't collide. Doc updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:59:56 +00:00
e86651723d Webhook: capture donor adult ticket names (Tickets 2026 update)
The updated Tickets 2026 form adds two donor (voucher) adult-ticket
name groups, names_Donor_1 / names_Donor_2, for the free adult
admissions. These were already counted via
item_quantity_adult_ticket_donor but their attendee names weren't
captured — added them to the adult-name list so they show at the gate.
Doc updated (new name fields + note that pure pricing line items and
payment_donor_voucher1/2 are ignored; the vouchers hidden count is
authoritative). No other schema changes needed — counts, extras,
donor, ice, and voucher handling already matched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 04:33:21 +00:00
1ba3f9ad1c Ticket vouchers now return remaining and decrement on use
The ticket-vouchers lookup previously returned the tier entitlement
every time, so a donor could keep claiming free tickets by re-
submitting the form. It now subtracts vouchers already consumed:

  remaining = entitled - used

where `used` is the sum of the Vouchers column across that donor's
prior ticket orders (each checkout stores what it applied). Response
gains entitled/used/remaining; `vouchers` is now the remaining count
the form should grant. Consumption is implicit — no counter to keep in
sync — and resets by zeroing/deleting the Vouchers value on the order
row in NocoDB.

- nocodb: findByEmail + vouchersUsedByEmail (case-insensitive).
- 8 new tests (36 total). Doc updated with the new response + reset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 22:11:08 +00:00
267957d333 Non-food vendors get no entry ticket
Only food vendors receive gate passes. The /vendor-webhook/non-food
endpoint now acknowledges the submission ({"status":"ignored"}) and
issues nothing, instead of creating a 1-pass ticket — kept as a safe
no-op so an accidentally-wired FluentForms feed doesn't 404. Food
webhook unchanged. Doc + tests updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 21:57:22 +00:00
7296555964 Vendor webhooks, free kids through 12, multi-origin lookup CORS
Children now free through age 12:
- Scannable/paid ticket total = adults + youth 13-16 only; kids 12 &
  under (0-4, 5-9, 10-12) are stored but not counted (charging starts
  at 13). computeTotal + freeKidsCount in fields.ts, webhook guard,
  scan/admin badges, event-report labels, docs, and personas updated.

Vendor booth webhooks (vendors.beartariacampgrounds.com):
- New /vendor-webhook/food (2 named pass-holders) and
  /vendor-webhook/non-food (1 pass-holder), reusing WEBHOOK_SECRET.
  Booth name -> ticket title; each named person = one entry pass;
  tagged with a "Food Vendor"/"Vendor" Ticket Type (badge on scan +
  event-report rollup). Idempotent + QR email like the attendee hook.
- Extracted shared FluentForms parsing (nameGroup/qty/selected/
  addressLine/readDonor) into fluentforms.ts; attendee webhook now
  imports it. 13 new unit tests.

Public lookup CORS is now a comma-separated allowlist; the caller's
Origin is echoed only if it matches. tickets + vendors both allowed
on donor-eligibility and ticket-vouchers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 05:13:07 +00:00
43fddec286 Add event report dashboard, slide-out drawer, in-app comp portal + creator tracking
All checks were successful
Build Android APK / build-apk (push) Successful in 56m36s
- 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>
2026-07-13 17:30:18 +00:00
251edfce42 CI: upload APK to the Forgejo release via API instead of forgejo-release action
All checks were successful
Build Android APK / build-apk (push) Successful in 40m23s
The build succeeds but publishing failed with "$FORGEJO_PATH: ambiguous
redirect" — the moving actions/forgejo-release@v2 tag updated to a broken
version. Replace it with direct Forgejo API calls (create release, delete any
prior same-named asset, upload the APK) using the built-in token, so the last
step is under our control.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 05:54:36 +00:00
848c8c7ee8 compose: read env from backend/.env (single source of truth)
Was ./.env, which required a manual copy from backend/.env and could go stale
(e.g. a changed PORTAL_PASSWORD not taking effect). Point env_file straight at
backend/.env so there's one file to edit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 03:53:44 +00:00
4b78c82b35 Add /crush33 comp-ticket portal + ticket-type badge on scan
Some checks failed
Build Android APK / build-apk (push) Failing after 1h11m4s
Portal (/crush33): password-gated page (PORTAL_PASSWORD) for admins to create
entry-only tickets from just name + email, with a category
(Guest/Worker/Performer/Volunteer/Speaker). Creates a 1-admission ticket, emails
the QR, and shows the QR on-screen. New Ticket Type column.

Scanner: shows a prominent TYPE badge (🎭 PERFORMER, 🛠️ WORKER, …) on the
confirm + success screens and in admin, so staff can see it's a special ticket.
Added Worker + Performer personas to /test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 03:30:08 +00:00
b7c77afe02 Rework webhook + model for the 2026 Tickets form
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>
2026-07-13 03:23:06 +00:00
718d1515b0 docs: dedicated ticket-voucher lookup page under fluent
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>
2026-07-10 06:36:39 +00:00
0b4ad1c99f Add ticket-voucher entitlement lookup + document both public APIs
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>
2026-07-10 06:27:32 +00:00
42043957e0 Add bear + QR app icon
Some checks failed
Build Android APK / build-apk (push) Failing after 35m46s
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>
2026-07-09 16:42:35 +00:00
7a6653b658 Banquet email input floats above keyboard; scan only within the reticle
All checks were successful
Build Android APK / build-apk (push) Successful in 43m56s
- 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>
2026-07-09 06:42:07 +00:00
65aa878192 Fix data-corruption on PK-less table; scrub real emails from examples
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>
2026-07-08 22:27:09 +00:00
ba5cb8a9fb docs: tailor donor-discount snippet for FluentForms conditional pricing
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>
2026-07-08 22:13:00 +00:00
fb2fdcb6b8 Add secret-gated public donor-eligibility lookup for checkout discount
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>
2026-07-08 22:09:25 +00:00
774d00ff5b Add public /webhook-doc page documenting the FluentForms webhook
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>
2026-07-08 21:58:33 +00:00
d7fbb2a154 Add public /install page with Obtainium button
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>
2026-07-08 21:51:44 +00:00
83e2af6ed3 CI: install NDK+CMake via sdkmanager and self-heal corrupt cache
All checks were successful
Build Android APK / build-apk (push) Successful in 32m4s
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>
2026-07-08 18:38:09 +00:00
8b202e4942 Scanner: tappable start overlay so the camera can never silently hang
Some checks failed
Build Android APK / build-apk (push) Failing after 8m55s
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>
2026-07-08 17:51:42 +00:00
8f62a84e9b CI: cap Kotlin daemon + Gradle heap so the build fits the runner host
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>
2026-07-08 17:42:28 +00:00
e87be49ebd Fix web camera stuck on "Starting camera…"
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>
2026-07-08 17:26:00 +00:00
f14e45349e CI: cap runner parallelism; play success chime immediately on scan
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>
2026-07-08 17:04:52 +00:00
34c7e23179 Make the success sound a warm ascending major arpeggio
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>
2026-07-08 16:57:03 +00:00
0e8fe3bb9a 4-digit auto-submit PIN + operator name in audit logs
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>
2026-07-08 16:45:23 +00:00
336d2a5c83 Fix banquet mode ignored on web: use latest onScan in camera loop
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>
2026-07-08 16:31:25 +00:00
3583c2e15f CI: cache Android SDK/NDK, Gradle, and npm across APK builds
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>
2026-07-08 16:26:06 +00:00
1ca2c38fbf Fix login hang after PIN + enrich Banquet mode
Some checks failed
Build Android APK / build-apk (push) Failing after 57m31s
- 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>
2026-07-08 16:01:41 +00:00
dc39c41428 Fix blank web page: align react and react-dom versions
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>
2026-07-08 05:37:45 +00:00
dbc1ece1ee CI: force IPv4 for Gradle so Maven Central is reachable
Some checks failed
Build Android APK / build-apk (push) Failing after 1h21m22s
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>
2026-07-08 05:04:47 +00:00
b71dce0878 Add /test page with sample QR codes for scanner testing
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>
2026-07-08 04:47:47 +00:00
8d6dd56a1a Add self-contained runner deploy script for remote Ubuntu host
Some checks failed
Build Android APK / build-apk (push) Failing after 15m24s
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>
2026-07-08 04:19:14 +00:00
994aaa88d1 Move APK runner to a separate host; openssl-based keystore
- 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>
2026-07-08 04:16:37 +00:00
d293e53ee6 Add Check-in / Ice / Banquet scan modes
- 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>
2026-07-08 04:07:51 +00:00
b36d63a6a4 Add check-in audit logging + in-app history view
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>
2026-07-08 03:53:30 +00:00
3397e3e3ec Initial Camp Scan ticketing system
Backend (Fastify + TS): FluentForms webhook -> NocoDB row + QR + MailerSend
email; PIN auth; scan/lookup/redeem with per-code serialization; reusable QR
codes with count-based check-in; admin search.

App (Expo, one codebase): Android APK + iPhone PWA. Login, camera scanner
(native + web barcode-detector split), green/red overlay with sound + haptics,
admin lookup/redeem. Session token persisted per device.

Ops: multi-stage Dockerfile serving API + PWA same-origin, compose bound to
127.0.0.1; Forgejo Actions runner + tag-triggered signed APK build for Obtainium.
Docs in README.md and INSTALL.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 03:47:59 +00:00