No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Lukas Bachschwell 76a71bcc2a
Import image, delete scanns for admins
Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at>
2026-09-11 10:04:44 +02:00
app Import image, delete scanns for admins 2026-09-11 10:04:44 +02:00
cmd/skannerd Server Flattening, deploy setup, mage setup, pub upgrade and more 2026-09-10 23:29:11 +02:00
deploy Icons, deployment and more 2026-09-10 23:52:39 +02:00
docs Import image, delete scanns for admins 2026-09-11 10:04:44 +02:00
internal Import image, delete scanns for admins 2026-09-11 10:04:44 +02:00
scripts Build 4 another encoding bug 2026-09-11 08:59:49 +02:00
.dockerignore Server Flattening, deploy setup, mage setup, pub upgrade and more 2026-09-10 23:29:11 +02:00
.editorconfig Scaffold Skanner: Flutter scanning app and Go backend 2026-09-10 22:28:47 +02:00
.gitignore Import image, delete scanns for admins 2026-09-11 10:04:44 +02:00
android_testers.txt.example Import image, delete scanns for admins 2026-09-11 10:04:44 +02:00
CLAUDE.md Update all docs 2026-09-11 00:05:59 +02:00
go.mod Server Flattening, deploy setup, mage setup, pub upgrade and more 2026-09-10 23:29:11 +02:00
go.sum Server Flattening, deploy setup, mage setup, pub upgrade and more 2026-09-10 23:29:11 +02:00
mage.go Server Flattening, deploy setup, mage setup, pub upgrade and more 2026-09-10 23:29:11 +02:00
magefile.go Import image, delete scanns for admins 2026-09-11 10:04:44 +02:00
README.md Import image, delete scanns for admins 2026-09-11 10:04:44 +02:00

Skanner

Scan customer QR codes at a show — one person or a whole group at a time — and attach a dictated or typed note to each group. Scans reach the server as they happen; at the end of a show the server reconciles them into attendee records.

app/      Flutter client for iOS and Android
cmd/      skannerd — the server binary
internal/ JSON API, web admin, store, badge parsing
deploy/   Dockerfile, compose stack, Traefik labels
docs/     architecture, data model, API contract, deployment, app builds

Live at https://skanner.lbs.sh.

Quick start — backend

mage run                       # serves on :8080 against a throwaway ./tmp/dev.db

On first boot with no SKANNER_ADMIN_PASSWORD_HASH set, the server invents an admin password and logs it once. Open http://localhost:8080/admin/, create a show, activate it, and issue a device key on the Devices page.

mage lint                      # go vet + staticcheck + flutter analyze
mage test                      # go test -race + flutter test
mage smoke                     # end-to-end run against a real server process
./bin/skannerd device add "Front desk"     # issue a key from the CLI instead
mage adminHash                             # generate a permanent admin hash

mage -l lists everything.

Quick start — app

mage app:get                   # flutter pub get
cd app && flutter run          # on a real device: the camera is the point

mage app:android               # release APK into releases_android/
mage app:ios                   # signed App Store IPA

Release to TestFlight

$EDITOR app/pubspec.yaml                    # bump the build: 0.1.0+4 → 0.1.0+5
(cd app && flutter analyze && flutter test)
mage app:validate app:upload

Four things make the difference between that working and costing you a build:

  • Build numbers are permanent. Once App Store Connect has seen 0.1.0 (5), that pair is rejected forever. app:validate runs Apple's real ingest checks and reports the same ITMS errors while spending nothing, which is why it comes first.
  • Test before validating, not after. Validation checks the package against store rules and knows nothing about whether the app works. A build that destroys data validates perfectly.
  • One invocation, not two. mage app:validate app:upload builds the archive once — mg.Deps dedupes within a run — and mage stops at the first failing target, so a failed VERIFY aborts before altool is handed anything and the build number survives.
  • Bump in pubspec.yaml. It drives the Android versionCode too. SKANNER_BUILD_NUMBER in app/ios/appstore.env is the escape hatch for re-uploading without touching the pubspec, not the normal path.

Credentials live in the gitignored app/ios/appstore.env, and the matching AuthKey_<id>.p8 in ~/.appstoreconnect/private_keys — where altool finds it by filename. Rename it and authentication fails with an error that never mentions the filename.

Internal TestFlight testers skip Beta App Review entirely and get a build within minutes of processing; external testers need review on the first build of a version.

docs/APP_BUILD.md has the detail: signing, the four iOS usage strings and why the photo-library one must not be deleted, and the two Android manifest entries that fail silently if dropped.

How it fits together

app (Flutter)                          server (Go, one binary)
─────────────                          ───────────────────────
camera → scan → local SQLite     ──▶   POST /api/v1/sync  ──▶  SQLite (modernc)
            (source of truth,          idempotent upsert on      shows · scan_groups
             survives no network)      client-owned UUIDv7       scans · note_templates
                    │                                           devices · customers
              outbox worker    ◀──  GET /api/v1/show/active
              (retry + backoff)      (which show, which note form)
                                       /admin/*  operator UI

The badge QR carries the attendee's record inline, so both halves parse it — internal/badge on the server and app/lib/domain/attendee.dart on the phone, against shared fixtures. The operator sees a name the instant they scan, and end-of-show reconciliation needs no network.

The app owns the primary keys. That one decision is what makes the offline path safe: a re-sent batch upserts onto the same rows instead of duplicating them, so a phone can lose its connection mid-sync and simply try again.

Further reading: architecture · data model · API · deployment · app builds · handoff