Skip to content

feat(webapp): live in-app QR scanner with photo-upload fallback#26

Merged
cgoulart35 merged 1 commit into
masterfrom
feat/live-qr-scanner
Jun 14, 2026
Merged

feat(webapp): live in-app QR scanner with photo-upload fallback#26
cgoulart35 merged 1 commit into
masterfrom
feat/live-qr-scanner

Conversation

@cgoulart35

Copy link
Copy Markdown
Owner

What

Replaces the single-photo file-upload Scan flow with a live in-browser camera scanner that decodes continuously and fires the fight the instant a QR is in frame. Keeps photo-upload as a fallback.

Three tiers, best-effort down:

  1. Native BarcodeDetector — Chrome / Android (fast, on-device)
  2. Vendored jsQR — iOS Safari and anything else without BarcodeDetector
  3. Existing photo upload → /scan/ (cv2) — no camera / permission denied / desktop

Why

One-shot photos were unreliable (blur, framing, round-trip). The new scanner is an in-page <video> overlay, so:

  • More contained — the happy path never leaves the app (no OS camera sheet, no file picker); the post-scan location = is a same-origin /fight/ nav, so it stays inside the installed PWA window.
  • Less Pi load — decoding runs on the phone; the server does no QR image processing unless the photo fallback is used.

Notes

  • resolveFightUrl() is a shared guard that only follows a decoded value resolving to a same-origin /fight/ URL — hardens both the live and photo paths over the old unguarded location = text.
  • jsQR is vendored as a static asset (no runtime CDN dependency; the official npm dist is unminified, ~257 KB raw / ~60 KB gzipped). A small tests/test_static_assets.py guards its presence/sanity.
  • getUserMedia needs a secure context, which prod (HTTPS behind the Cloudflare tunnel) already serves. No API/route/requirements changes.

Verification

  • /test — full suite green (69 passed), incl. the new asset guard; test_qr_decode.py still covers the cv2 fallback.
  • Cross-phone QA on the real domain (qa sandbox): Android Chrome (BarcodeDetector), macOS Safari + iPhone Safari (jsQR, rear camera + inline playback), plus deny-permission → photo fallback and the "Use photo instead" button.

🤖 Generated with Claude Code

Replace the single-photo file-upload Scan flow with a live in-browser camera
scanner that decodes continuously and fires the fight the instant a QR is in
frame. Three tiers, best-effort down: native BarcodeDetector (Chrome/Android)
-> vendored jsQR (iOS Safari and others without BarcodeDetector) -> the existing
photo upload to /scan/ (cv2) when there's no camera / permission is denied / on
desktop.

The scanner is an in-page <video> overlay, so the happy path never leaves the
app (no OS camera sheet, no file picker) and decoding runs on the phone -- the
server does no QR image processing unless the photo fallback is used. A shared
resolveFightUrl() guard only follows a decoded value that resolves to a
same-origin /fight/ URL, hardening both the live and photo paths over the old
unguarded `location = text`.

- jsQR vendored as a static asset (no runtime CDN dependency); guarded by a
  small presence/sanity test.
- getUserMedia requires a secure context, which prod (HTTPS) already serves.
- Verified cross-phone: Android Chrome (BarcodeDetector), macOS Safari and
  iPhone Safari (jsQR, rear camera + inline playback), plus the deny-permission
  and "Use photo instead" fallbacks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/app/static/index.js
@claude

claude Bot commented Jun 14, 2026

Copy link
Copy Markdown

🔴 One must-fix: decodeWithJsQR in index.js is missing the jsQR(...) call — code is never declared, so every scan tick throws a ReferenceError and the rAF loop dies silently. This completely breaks the jsQR fallback path (iOS Safari, any browser without BarcodeDetector). Fix: add var code = jsQR(imageData.data, width, height); before the return. See inline comment.

@cgoulart35
cgoulart35 merged commit 1a0387d into master Jun 14, 2026
5 checks passed
@cgoulart35
cgoulart35 deleted the feat/live-qr-scanner branch June 14, 2026 23:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant