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

314 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](../PROJECT.md);
for journeys / step-by-step flows see [USER_JOURNEYS.md](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](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-out**`new-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/](../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](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](../PROJECT.md):
- 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
- [USER_JOURNEYS.md](USER_JOURNEYS.md) — step-by-step flows for every supported scenario.
- [CONCEPT_MOBILE_UI.md](CONCEPT_MOBILE_UI.md) — design reference for the mobile layout.
- [CONCEPT_HTML_VIEWER.md](CONCEPT_HTML_VIEWER.md) — export-viewer design.
- [CONCEPT_DIASHOW.md](CONCEPT_DIASHOW.md) — planned diashow design.
- [IDEAS.md](IDEAS.md) — speculative extensions (global diashow, reactions, multi-tenancy, ...).
- [PROJECT.md](../PROJECT.md) — full architectural blueprint and rationale.
- [TEST_GUIDE.md](../TEST_GUIDE.md) — manual smoke-test script for the main flows.