Files
EventSnap/docs/FEATURES.md
MechaCat02 9a0ceeced7 docs: realign blueprint with shipped state + add feature/journey/ideas docs
- PROJECT.md, README.md, TEST_GUIDE.md: status line refreshed; rate-limiter
  doc-vs-code drift fixed; HTML export section rewritten for the SvelteKit-
  static viewer; SSE event names + new events documented; config seed block
  extended with planned toggles + privacy_note; decision log entries added.
- docs/CONCEPT_HTML_VIEWER.md, docs/CONCEPT_MOBILE_UI.md: banner the design
  intent as shipped; point at the source-of-truth code paths.
- docs/CONCEPT_DIASHOW.md: planned-then-shipped design for the live diashow
  (two-queue policy, pluggable transitions, data-mode aware).
- docs/FEATURES.md: capability matrix by role (Guest / Host / Admin) plus
  prose per area (auth, posting, feed, moderation, admin, export, gestures,
  data mode, quotas, privacy note, extensibility).
- docs/USER_JOURNEYS.md: step-by-step flows for every supported scenario,
  including PIN reset by host, data mode, privacy note, gestures, and the
  admin toggles.
- docs/IDEAS.md: speculative extensions (global diashow, reactions,
  multi-tenancy, animation pack, etc.) — explicitly out of v0.16 scope.
- backend/migrations/README.md, frontend/src/lib/README.md: codify the
  "never edit a shipped migration" rule and the lib/ conventions
  (one store per concern, gestures via actions, sheets via ContextSheet,
  transitions as drop-in components).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:31:06 +02:00

26 KiB
Raw Permalink Blame History

EventSnap — Feature Set & Capability Matrix

This document is the authoritative, code-cross-checked summary of what EventSnap can do today and what is planned. For the design rationale of each area see PROJECT.md; for journeys / step-by-step flows see USER_JOURNEYS.md.

Status legend: ✓ shipped · ◐ partial · ◯ planned · ✗ out of scope


1. Capability matrix by role

Capability Guest Host Admin Notes
Onboarding & sessions
Join via shared event link / QR code Name-only registration; server issues JWT + 4-digit PIN
First-visit guided tour (4 steps) Dismissed once, flag in localStorage
Persistent 30-day session JWT in localStorage; refreshed on activity
Sign in on another device using name + PIN 3 wrong PINs → 15-min lockout
"Ich habe bereits einen Account" link on the join page Small inline link → /recover (name + PIN)
View / copy own PIN any time ("My Account") Read from localStorage; never sent back from the server
Log out / "Leave event" Confirmation bottom-sheet; invalidates the session row
Rename own display name Not yet wired; PIN-protected change
Pick data mode (Saver / Original) in My Account Saver = compressed (default). Original = full files + data-usage warning. Applies to feed and diashow. Per-device, in localStorage
Read the Datenschutzhinweis (privacy note) Free text set by Admin during setup; rendered preformatted in My Account; first-visit guide briefly points to it
Admin password login (separate route) 1-day token; lives in sessionStorage
Reset another user's PIN (one-time display modal) ✓* Host: guests only. Admin: hosts + guests. New PIN shown once to the requester; user signs in with it; PIN is stored on their device on next login. * Host cannot reset another Host's PIN
Posting
Pick photos/videos from device library (multi-select) Bottom-sheet source picker
In-app camera capture (getUserMedia) Front/back toggle, photo, MediaRecorder video
Caption + #hashtag extraction Optional; hashtags parsed server-side
Edit own caption / hashtags after upload PATCH /api/v1/upload/{id}
Delete own upload Long-press on the card (or the kebab menu on desktop) → Löschen in the context sheet. Comment-style trash icon also available on each post elsewhere as it's added.
Delete own comment Trash icon in lightbox
Background upload queue (survives reload) IndexedDB-persisted, sequential, retry
Rate-limit auto-resume banner Countdown above bottom nav; resumes when window opens
Chunked / resumable upload for > 100 MB Planned (v1.x)
Feed & social
Chronological list feed (full-width cards) Default view, infinite scroll
3-column grid feed with toggle Video play badges, duration
Search & autocomplete (uploader + hashtag) Grid view; derived in-memory, no extra API calls
Active filter chips (OR within type, AND across types) Multiple hashtags = OR; uploader + hashtag = AND
Fullscreen lightbox with swipe Swipe navigates the filtered set
Like / unlike any post Single toggle; SSE like-update
Read comments on any post
Add a comment Hashtags in comments also parsed
Real-time feed via SSE new-upload, new-comment, like-update, upload-processed, pin-reset, event-updated, etc.
Pause SSE when app is backgrounded Page Visibility API; reconnect on foreground
Delta-fetch (/feed/delta?since=) on reconnect Runs on every visibility-restore; merges new + deleted uploads
Individual file download button per post "Original anzeigen" in the post context sheet — streams via /api/v1/upload/{id}/original
Live diashow (see CONCEPT_DIASHOW.md)
Start fullscreen auto-advancing slideshow Two queues: live (SSE) drains first, shuffle as fallback. Crossfade + Ken Burns transitions; pluggable. Respects data mode.
Moderation (Host)
List all event users Includes upload count, total bytes
Ban / unban a user Modal asks: hide their existing uploads, or keep visible?
Delete any upload
Delete any comment
Promote guest to Host
Demote Host to guest Hosts may demote other Hosts. Cannot demote self. Admins cannot be demoted by hosts.
Reset a guest's PIN (Host) / any non-admin PIN (Admin) New PIN shown once in modal; Host shows/shares it with the guest
Lock new uploads ("Event schließen") Likes + comments + browsing remain open
Unlock new uploads
Release gallery → trigger export generation Enqueues both ZIP and HTML-viewer jobs
Instance configuration (Admin)
Live disk-usage / user / upload / banned stats Stats tab; queries sysinfo
Edit per-file limits (image MB / video MB) Config tab; hot-reloadable from DB
Edit per-endpoint rate limits Upload/hour, feed/min, export/day
Toggle all rate limits on/off Master switch — when off, every limiter passes through
Toggle individual rate limits on/off Per-endpoint switch (upload / feed / export / join)
Toggle quota enforcement on/off (master + per-area) Master switch + per-area (storage / upload count). When off, nothing is enforced
Edit quota tolerance Live (free_disk × tolerance) / active_uploaders formula enforced on upload
Edit estimated guest count
Edit compression-worker concurrency
Edit Datenschutzhinweis (privacy note, free text) Plain text, whitespace + newlines preserved, no HTML. SSE event-updated broadcasts edits live.
Inspect export job list & progress
Low-disk alert (< 10 GB free) Planned
Event banner / cover image DB column exists, no UI
Quota visibility (Guest-facing)
Show current per-user quota estimate "Du hast X MB von Y MB genutzt." in My Account and on the upload screen. Computed from the live formula. Hidden when quota enforcement is toggled off
Export
Wait at locked export page until released Friendly "not yet available" copy
Download Gallery.zip (full-quality originals) Streamed via async-zip; Photos/ + Videos/ folders
Download Memories.zip (offline HTML viewer) Self-contained SvelteKit-static app + data.json + media/
HTML-export in-app guide modal before download Explains: unzip first, open index.html
Per-IP export download rate limit (3 / day)
Banned guest (subset)
Cannot upload, like, or comment Returns HTTP 403
Can browse the feed
Can still download the export once released Spec design choice

2. Feature areas in detail

2.0 Touch-first interactions (mobile) vs. buttons (desktop)

EventSnap is mobile-first. Where it makes the UI cleaner, primary actions are reached via gestures on touch devices, with conventional buttons mirrored on tablet/desktop:

  • Long-press on a post → context bottom sheet ("Löschen", "Original anzeigen", report, share). On desktop the same actions are a kebab/⋯ menu in the card's corner.
  • Long-press on a comment → context sheet with "Löschen" (own comments only) and "Kopieren".
  • Swipe left/right in the lightbox → navigate the filtered set.
  • Swipe down on a bottom sheet → dismiss.
  • Pull-to-refresh on the feed → force a delta-fetch even when SSE is up.
  • Double-tap on a post → like (Instagram-style), with a heart-burst animation. Tap the heart icon as the explicit alternative.

Design rule: gestures should always have a discoverable button equivalent somewhere on the page, so the app stays usable on a stylus, mouse, or for users who don't know the gesture vocabulary. Take inspiration from Instagram, WhatsApp, and Telegram for the "feels right" baseline — long-press for context, swipe to dismiss, double-tap to react.

2.1 Authentication and identity

EventSnap's identity model is "a name + a 4-digit PIN, scoped to one event". There is no email, no password, no account portal.

  • Joining. On the join page the user types a display name. The server creates a user row, generates a 4-digit PIN, stores bcrypt(pin), signs a 30-day JWT, and returns the PIN in clear text once in the response. The client persists the JWT and the PIN to localStorage.
  • PIN visibility. The PIN is shown to the user prominently once at registration, and remains visible in the My-Account page (read directly from localStorage — never sent back from the server).
  • Returning on the same device. A valid JWT in localStorage → straight to the feed.
  • Returning on a new device. Type the name on the join page → server detects the existing user → user is prompted for their PIN. bcrypt.verify → new JWT, fresh device is now bound to the same account.
  • Lockout. 3 wrong PIN attempts → 15-minute lockout per user (pin_locked_until column, migration 006).
  • Name collisions. Names are unique per event (case-insensitive, migration 007). If someone tries to join with a name already taken, the join page automatically presents the PIN-recovery form for that account ("Already taken — sign in instead, or pick another name"). The join page also surfaces an explicit "Ich habe bereits einen Account" link routing to /recover for users who already know they want to sign in.
  • PIN reset by Host / Admin. Planned. If a guest loses their PIN and localStorage is gone everywhere, a Host (for guests) or Admin (for hosts and guests) can hit a PIN zurücksetzen action in the user list. A fresh PIN is generated server-side, its bcrypt stored, and the plaintext is shown once in a modal to the requesting operator. The operator shows / sends the new PIN to the user, who then signs in via /recover — the PIN is persisted to localStorage on that device on a successful recovery, exactly like a brand-new join. Host cannot reset another Host's PIN; only Admins can.
  • Roles. guest (default), host, admin. The Admin role is seeded from the ADMIN_PASSWORD_HASH env var; admins log in at /admin/login with a password (separate JWT, 1-day expiry, in sessionStorage). Hosts are guests promoted by an admin. Hosts may also demote other Hosts to guests (planned) — but never themselves, to avoid locking the event out of moderation. Admins can demote anyone except admins.

2.2 Posting pipeline

The upload pipeline is built for flaky mobile networks:

  1. Source picker (bottom sheet from the FAB): camera or gallery.
  2. Preview screen — staged files appear as thumbnails; user can remove individuals, add a caption (with #hashtags), and tap quick-tag chips derived from the caption.
  3. Submit — the client immediately returns to the feed (optimistic UX). Files enter an IndexedDB-persisted queue.
  4. Queue worker — runs sequentially (one upload at a time), per-file progress via XHR. Survives reloads and app backgrounding. A red badge on the FAB indicates active uploads.
  5. Server-side processing — multipart received → MIME-sniffed via infer → size validated → original stored → compression worker (bounded by a tokio::sync::Semaphore) resizes to an 800-px preview (images via the image crate + oxipng for PNG) or extracts a frame at the 1-second mark (videos via ffmpeg). Status is tracked in the new compression_status column (migration 008).
  6. Real-time fan-outnew-upload SSE first (no preview yet), then upload-processed when the preview/thumbnail is ready, so clients can swap a placeholder for the real image without re-fetching the feed.
  7. Rate-limit-aware client — when the server returns HTTP 429 with Retry-After, the queue parks remaining items and shows an inline countdown banner; uploads resume automatically.

2.3 Feed

  • Two layouts — chronological list (default) and 3-column grid. Toggle in the header.
  • List view has no search; it's the consumption-focused mode (like an Instagram feed).
  • Grid view has the search bar — autocomplete suggestions are computed in-memory from the loaded uploads, so typing never hits the server.
  • Filter chips — multiple hashtags combine with OR; multiple uploaders combine with OR; hashtag + uploader combine with AND. Matches the redesign concept exactly.
  • Lightbox — fullscreen view, swipe navigates the filtered set, with embedded like/comment UI.
  • Real-time — SSE delivers new-upload, upload-processed, like-update, new-comment, upload-deleted, event-closed/event-opened, export-progress, export-available. Client pauses SSE on visibilitychange: hidden and reopens on visible.

2.4 Host / Admin tooling

  • Host dashboard — three collapsible sections: Stats, Event-Einstellungen, Nutzerverwaltung. Ban modal asks explicitly whether to hide the user's existing uploads from the public feed. Promote/demote, lock/unlock, release-gallery are one-tap.
  • Admin dashboard — same dashboard plus three more inner tabs (Stats, Config, Export, Nutzer). Config form covers per-file limits, rate limits, quota tolerance, estimated guest count, and compression concurrency — all stored in the config table and read on each request, so changes take effect without a restart. Disk widget pulls from the sysinfo crate live.

2.5 Data mode (planned)

Each device picks a data mode in My Account; the setting lives in localStorage so a guest can be on Saver on their phone and Original on their laptop.

Mode Default? Feed loads... Lightbox / diashow loads... Warning shown?
Datensparer (Saver) preview (compressed) preview no
Original original original yes — "kann mobile Datennutzung erhöhen" once on enable

Applies uniformly to the live app's feed/lightbox and the diashow. The viewer (offline HTML export) is unaffected — it's already a snapshot of pre-bundled media variants.

2.6 Rate limits and quotas — toggleable (planned)

The Admin Config tab gains explicit on/off toggles in addition to the numeric inputs:

  • Master switch — all rate limits. When off, every limiter middleware short-circuits to pass-through. Useful for testing or trusted internal events.
  • Per-endpoint switches. Upload / feed / export / join each have their own toggle. The numeric input becomes informational while the toggle is off.
  • Master switch — quotas. When off, no quota check ever runs.
  • Per-area quota switch. Storage-bytes quota and upload-count quota can be disabled independently.

When a feature is toggled off, the relevant UI in the guest-facing app should adapt: e.g. the "Du hast X von Y MB genutzt" widget hides itself when storage quota is disabled. The quota estimate is computed from the same formula the server uses ((free_disk × tolerance) / max(active_uploaders, 1)) — surfaced in My Account and on the upload preview screen so guests know before they pick files.

2.7 Privacy note (Datenschutzhinweis, planned)

Admin sets a free-text Datenschutzhinweis during instance setup (Admin Dashboard → Config). It's stored as a single config key (plain text, whitespace and newlines preserved, no HTML). Guests see it in their My Account page, rendered inside a preformatted block — no parsing, no markdown, just exactly what the admin typed. The first-visit onboarding guide gains a one-line nudge: "Datenschutzhinweis findest du in deinem Account."

Rationale: many real events (in Germany especially) need a per-event privacy statement without the operator wanting to ship a separate static page or rebuild the app.

2.8 Export

Two artifacts, both generated on demand after the host taps "Release gallery":

  • Gallery.zip — full-quality originals only, structured into Photos/ and Videos/, filenames {date}_{time}_{username}_{id}.{ext}, streamed via async-zip with no full archive in memory.
  • Memories.zip — the offline HTML viewer. Pre-built SvelteKit-static app from frontend/export-viewer/, bundled with a generated data.json snapshot and a media/ folder of thumbnails + full-size variants. Open index.html in any browser — no server required, no internet required. List/grid views, lightbox, hashtag chips, like counts, comments — all visually matched to the live app.

The export page shows live progress (SSE) while jobs run, then becomes a download button when complete.


2.9 Maintainability and extensibility

EventSnap is small enough to be a single-developer project; it should stay easy to extend. A few principles to keep adding features cheap:

  • Diashow transitions are drop-in components. Each animation implements a small interface and lives under frontend/src/lib/diashow/transitions/. Adding a transition is one file + one entry in a registry.
  • Feature toggles live in the config table. Today's rate-limit and quota switches follow the same pattern any new opt-in feature would use — no redeploy to flip behaviour.
  • One Svelte store per cross-cutting concern. Auth, upload queue, SSE, data mode, diashow state — composable rather than copy-pasted into each route.
  • Migrations are append-only. Never edit a shipped migration; always add a new pair.
  • Background jobs share one pipeline. Export and compression already publish progress via the export_job row + SSE; future long-running work (analytics, archival) should plug into the same shape.

See IDEAS.md for a longer riff on these patterns.

3. Out of scope (intentionally not built)

These are explicit non-goals from PROJECT.md §4:

  • Native iOS / Android apps
  • Multiple simultaneous events (multi-tenancy)
  • Email-based auth / password reset
  • Push notifications
  • User-to-user direct messaging
  • Payment / monetisation
  • CI/CD pipeline
  • "Save to camera roll" automation on iOS/Android — guests download the ZIP and use their platform file manager

4. See also