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>
This commit is contained in:
Hank 2026-07-08 03:47:59 +00:00
commit 3397e3e3ec
60 changed files with 14703 additions and 0 deletions

24
ci/set-version.mjs Normal file
View file

@ -0,0 +1,24 @@
// Set app.json version + Android versionCode from a git tag before prebuild.
// Usage: node ci/set-version.mjs v1.2.3
// versionName = 1.2.3 ; versionCode = 1*1_000_000 + 2*1_000 + 3 (monotonic,
// so Obtainium installs each new release over the previous one).
import { readFileSync, writeFileSync } from "node:fs";
const raw = process.argv[2] || "v0.0.0";
const m = raw.replace(/^v/, "").match(/^(\d+)\.(\d+)\.(\d+)/);
if (!m) {
console.error(`set-version: cannot parse tag "${raw}"`);
process.exit(1);
}
const [, MA, MI, PA] = m.map(Number);
const versionName = `${MA}.${MI}.${PA}`;
const versionCode = MA * 1_000_000 + MI * 1_000 + PA;
const path = new URL("../app/app.json", import.meta.url);
const cfg = JSON.parse(readFileSync(path, "utf8"));
cfg.expo.version = versionName;
cfg.expo.android = cfg.expo.android || {};
cfg.expo.android.versionCode = versionCode;
writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
console.log(`set-version: versionName=${versionName} versionCode=${versionCode}`);