feat: organizer Android app and token-gated upload API
CI / lint-and-test (push) Failing after 1m18s

Replace Syncthing as the product upload path with a share-target app
and POST /api/jobs. Web UI live-polls job progress.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Frank Schwenk
2026-08-27 21:38:50 +02:00
parent 5dcc3717c7
commit c03f17488c
58 changed files with 3134 additions and 186 deletions
+35 -28
View File
@@ -6,31 +6,33 @@
## One-Liner
Event photo pep pipeline: Syncthing drop -> gmic/rembg -> web. Upload a
photo at the event, get back weird/fun filtered variants seconds later.
Event photo pep pipeline: organizer Android app -> gmic/rembg -> web.
Upload a photo at the event, get back weird/fun filtered variants seconds
later.
## Vision
At an event (f12 meetup/party), someone drops a photo into a Syncthing
folder from a phone/camera rig. A worker rips the background off, runs it
through a random gmic filter/blend chain, and a handful of variants show
up on a shared web page almost immediately — no app install, no login, no
waiting for someone to "process the photos later". If a variant is fun,
remix it with different filters right there. Finished jobs sync back to
the phone via the same Syncthing share.
At an event (f12 meetup/party), the organizer shares a photo into the
Android app. The phone converts it to a 2000px JPEG, uploads it, a worker
rips the background off, runs it through a random gmic filter/blend chain,
and a handful of variants show up on a shared web page almost immediately —
guests need no app, no login, no "process the photos later". If a variant
is fun, remix it with different filters right there. Finished jobs land on
the organizer's phone (gallery + local notification) and stay on the web
for everyone with the link.
## Audience
Event organizers and attendees at f12 events, on their phones, often on
bad venue wifi. Nobody signs up, nobody logs in. Whoever has the link
(and whoever's syncing photos in) is the whole audience.
Event organizers (upload via the Android app) and attendees on their
phones, often on bad venue wifi. Nobody signs up, nobody logs in. Guests
with the link see everything; only the organizer's app can upload.
## Tone & Wording
- **Voice:** direct, casual, no explaining-the-obvious. It's a fun tool
for an event, not enterprise software.
- **Formality:** du. Short German labels in the UI (Ansehen, Remix,
Download, Varianten, Zwischenschritte).
Download, Varianten, Zwischenschritte, Warteschlange, Fertig).
- **Error messages:** say what broke, don't blame the visitor — most
errors here are gmic filter flakiness, not user error.
- **Forbidden words/phrases:** no corporate buzzwords, no "Erlebnis",
@@ -42,7 +44,7 @@ bad venue wifi. Nobody signs up, nobody logs in. Whoever has the link
|---------|------|-----|
| Success | `Remix erzeugt.` | `Dein Kunstwerk ist fertig! 🎉` |
| Error | `Foreground-Filter fehlgeschlagen: Timeout nach 120s` | `Etwas ist schiefgelaufen.` |
| Empty state | `Noch keine Jobs. Bild in den Sync-Ordner legen, dann kurz warten.` | `Hier ist noch nichts los... lade doch was hoch! 😊` |
| Empty state | `Noch keine Jobs. Foto in der App teilen, dann kurz warten.` | `Hier ist noch nichts los... lade doch was hoch! 😊` |
## Design
@@ -54,46 +56,51 @@ bad venue wifi. Nobody signs up, nobody logs in. Whoever has the link
## Non-Goals
- No web auth / accounts / login product — anyone with the link sees
everything, by design, for this event tool.
- No camera-to-phone client in this repo — upload path is Syncthing
(phone/folder setup is out of band).
- No web auth / accounts / login product — anyone with the link **sees**
everything, by design. Upload is a separate shared token in the
organizer app, not a user account.
- No Play Store / FCM / ntfy dependency. Notifications are local Android.
- No beamer/projector product built into this repo.
- Not a DAM (digital asset manager) — no albums, tagging, search,
retention policies. `incoming/`, `variants/`, and `intermediates/`
are the whole data model.
- No auto-deletion of incoming uploads. Ever. The worker only reads/copies.
- No Syncthing container in this compose — Syncthing runs on the host.
- No Syncthing container in this compose. A host Syncthing share can
still write `incoming/` as a fallback; it is not the product path.
## Infrastructure (project-local)
- Workdir on server: `/home/frank/live.f12.rocks`
- Data volume (Syncthing share): `${DATA_HOST_DIR}` — prod default
- Data volume: `${DATA_HOST_DIR}` — prod default
`/home/frank/sync.schwenk.online/data/livef12`, local override `./data`.
Bind-mounted into `worker` and `web` at `/data` (see `compose.yml`).
- Layout under the share:
- `incoming/` — phone drop
- Layout:
- `incoming/` — HTTP upload (and optional Syncthing drop)
- `variants/` — `{stem}_original.jpg`, `{stem}_rembg.png`, `{stem}_vN.png`
- `intermediates/` — `{stem}_vN_*.png` (incl. `_post_0` …)
- `meta/` — web bookkeeping JSON (optional `.stignore` on phone)
- `meta/` — status + manifest JSON
- `processed.json`
- Domain: `live.f12.rocks`, routed via the shared external `traefik`
network, TLS via `myresolver` (see `INFRASTRUCTURE.md`).
- rembg model cache lives in the named Docker volume `rembg_cache`, not
under the Syncthing share — container state, not event data.
- rembg model cache lives in the named Docker volume `rembg_cache`.
- Organizer Android app lives in `android/` (sideload APK).
- `UPLOAD_TOKEN` in server `.env` — required for `POST /api/jobs`.
## Project-Specific Rules
```markdown
- Never delete or move files under incoming/ from worker code — Syncthing
is the writer there. (reason: uploads are the one copy of the original
that exists outside a phone's camera roll)
- Never delete or move files under incoming/ from worker code — the
upload API (and optional Syncthing) is the writer there. (reason:
uploads are the one copy of the original that exists outside a
phone's camera roll)
- Keep every intermediate image under intermediates/ — don't clean them
up automatically. (reason: useful for debugging bad filter picks,
disk is cheap compared to re-running gmic)
- gmic/rembg subprocess calls always run under `nice` (see NICE_LEVEL) —
the worker box needs to stay responsive for other things during an
event. (reason: explicit requirement, avoid CPU starvation)
- POST /api/jobs is fail-closed: empty UPLOAD_TOKEN disables uploads.
The public web UI stays read-only.
```
---