docs: correct the claims that no longer match the code

Checked each against the implementation and fixed the doc, never the code:

- FEATURES claimed the ban modal offers a choice about hiding existing uploads. There
  is no such choice — a ban always hides. USER_JOURNEYS §9 was already right.
- FEATURES claimed hosts may demote other hosts. It is admin-only, enforced in the
  backend, and the two documents contradicted each other on it.
- FEATURES showed the quota widget as guest-facing; it is deliberately staff-only.
- The first-visit tour has six steps, not four.
- USER_JOURNEYS §12.7 said export downloads are rate-limited per IP. They are per USER
  — a materially different thing at a shared-NAT venue, where per-IP would have locked
  out the fourth guest to fetch their keepsake.
- §15 still described "Event verlassen"; that button is now Abmelden / Auf allen
  Geräten abmelden.
- §4, §13, §14, §16 and §18 were marked "(planned)" and have shipped.
- The lightbox row now describes what exists after this branch: prev/next controls,
  arrow keys and swipe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Fabian Hamm (Privat)
2026-08-03 18:37:09 +02:00
parent 2b1500e624
commit 46bb2e5174
2 changed files with 98 additions and 57 deletions

View File

@@ -14,12 +14,12 @@ Status legend: **✓ shipped** · **◐ partial** · **◯ planned** · **✗ ou
|---------------------------------------------------------|:-----:|:-----:|:-----:|-----------------------------------------------------------------------| |---------------------------------------------------------|:-----:|:-----:|:-----:|-----------------------------------------------------------------------|
| **Onboarding & sessions** | | | | | | **Onboarding & sessions** | | | | |
| Join via shared event link / QR code | ✓ | ✓ | ✓ | Name-only registration; server issues JWT + 4-digit PIN | | 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` | | First-visit guided tour (6 steps) | ✓ | ✓ | ✓ | Welcome, upload, hashtags, long-press, hell/dunkel, PIN (+ privacy-note pointer when one is set). Dismissed once, flag in `localStorage` |
| Persistent 30-day session | ✓ | ✓ | ✓ | JWT in `localStorage`; refreshed on activity | | Persistent 30-day session | ✓ | ✓ | ✓ | JWT in `localStorage`; refreshed on activity |
| Sign in on another device using name + PIN | ✓ | ✓ | ✓ | 3 wrong PINs → 15-min lockout | | 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) | | "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 | | 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 | | Log out ("Abmelden" / "Auf allen Geräten abmelden") | ✓ | ✓ | ✓ | Confirmation bottom-sheet; `DELETE /session` (this device) or `DELETE /sessions` (all devices). Nothing is deleted — account, PIN and uploads stay |
| Rename own display name | ◯ | ◯ | ◯ | Not yet wired; PIN-protected change | | 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` | | 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 | | Read the **Datenschutzhinweis** (privacy note) | ✓ | ✓ | ✓ | Free text set by Admin during setup; rendered preformatted in My Account; first-visit guide briefly points to it |
@@ -42,7 +42,7 @@ Status legend: **✓ shipped** · **◐ partial** · **◯ planned** · **✗ ou
| 3-column grid feed with toggle | ✓ | ✓ | ✓ | Video play badges, duration | | 3-column grid feed with toggle | ✓ | ✓ | ✓ | Video play badges, duration |
| Search & autocomplete (uploader + hashtag) | ✓ | ✓ | ✓ | Grid view; derived in-memory, no extra API calls | | 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 | | Active filter chips (OR within type, AND across types) | ✓ | ✓ | ✓ | Multiple hashtags = OR; uploader + hashtag = AND |
| Fullscreen lightbox with swipe | ✓ | ✓ | ✓ | Swipe navigates the filtered set | | Fullscreen lightbox with prev/next navigation | ✓ | ✓ | ✓ | Navigates the *filtered* set. On-screen prev/next controls, ← / → arrow keys, and swipe left/right on touch |
| Like / unlike any post | ✓ | ✓ | ✓ | Single toggle; SSE `like-update` | | Like / unlike any post | ✓ | ✓ | ✓ | Single toggle; SSE `like-update` |
| Read comments on any post | ✓ | ✓ | ✓ | | | Read comments on any post | ✓ | ✓ | ✓ | |
| Add a comment | ✓ | ✓ | ✓ | Hashtags in comments also parsed | | Add a comment | ✓ | ✓ | ✓ | Hashtags in comments also parsed |
@@ -56,11 +56,11 @@ Status legend: **✓ shipped** · **◐ partial** · **◯ planned** · **✗ ou
| | | | | | | | | | | |
| **Moderation (Host)** | | | | | | **Moderation (Host)** | | | | |
| List all event users | | ✓ | ✓ | Includes upload count, total bytes | | List all event users | | ✓ | ✓ | Includes upload count, total bytes |
| Ban / unban a user | | ✓ | ✓ | Modal asks: hide their existing uploads, or keep visible? | | Ban / unban a user | | ✓ | ✓ | Ban **always hides** their existing uploads — no opt-out, the modal is a plain confirm. Unban restores them. Both invalidate and rebuild a released keepsake. See USER_JOURNEYS §9/§10 |
| Delete any upload | | ✓ | ✓ | | | Delete any upload | | ✓ | ✓ | |
| Delete any comment | | ✓ | ✓ | | | Delete any comment | | ✓ | ✓ | |
| Promote guest to Host | | ✓ | ✓ | | | Promote guest to Host | | ✓ | ✓ | |
| Demote Host to guest | | | ✓ | Hosts may demote other Hosts. Cannot demote self. Admins cannot be demoted by hosts. | | Demote Host to guest | | | ✓ | **Admin only** — the backend 403s a Host demoting a peer Host (F1: it would launder past the ban / PIN-reset peer guards). Nobody may change their own role; Admins cannot be demoted at all. The button is hidden for non-admin 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 | | 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 | | Lock new uploads ("Event schließen") | | ✓ | ✓ | Likes + comments + browsing remain open |
| Unlock new uploads | | ✓ | ✓ | | | Unlock new uploads | | ✓ | ✓ | |
@@ -78,18 +78,18 @@ Status legend: **✓ shipped** · **◐ partial** · **◯ planned** · **✗ ou
| Edit compression-worker concurrency | | | ✓ | | | Edit compression-worker concurrency | | | ✓ | |
| Edit **Datenschutzhinweis** (privacy note, free text) | | | ✓ | Plain text, whitespace + newlines preserved, no HTML. SSE `event-updated` broadcasts edits live. | | Edit **Datenschutzhinweis** (privacy note, free text) | | | ✓ | Plain text, whitespace + newlines preserved, no HTML. SSE `event-updated` broadcasts edits live. |
| Inspect export job list & progress | | | ✓ | | | Inspect export job list & progress | | | ✓ | |
| Low-disk alert (< 10 GB free) | | | ◯ | Planned | | Low-disk alert | | | | Red banner at the top of the Host dashboard when `disk_low`, naming free space and the keepsake's required bytes. Fails closed to "not low" on an unreadable mount |
| Event banner / cover image | | | ◯ | DB column exists, no UI | | Event banner / cover image | | | ◯ | DB column exists, no UI |
| | | | | | | | | | | |
| **Quota visibility (Guest-facing)** | | | | | | **Quota visibility (staff-only)** | | | | |
| 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 | | Show current per-user quota estimate | | ✓ | ✓ | **Deliberately not shown to guests** — the storage widget in My Account and on the upload screen is gated on host/admin. Computed from the live formula; also hidden when quota enforcement is toggled off |
| | | | | | | | | | | |
| **Export** | | | | | | **Export** | | | | |
| Wait at locked export page until released | ✓ | ✓ | ✓ | Friendly "not yet available" copy | | 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 `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/` | | 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` | | HTML-export in-app guide modal before download | ✓ | ✓ | ✓ | Explains: unzip first, open `index.html` |
| Per-IP export download rate limit (3 / day) | ✓ | ✓ | ✓ | | | Per-**user** export download rate limit (3 / day) | ✓ | ✓ | ✓ | Keyed on the user id, not the IP — a venue behind one NAT would otherwise share a 3/day budget across every guest |
| | | | | | | | | | | |
| **Banned guest** (subset) | | | | | | **Banned guest** (subset) | | | | |
| Cannot upload, like, or comment | ✗ | | | Returns HTTP 403 | | Cannot upload, like, or comment | ✗ | | | Returns HTTP 403 |
@@ -143,7 +143,7 @@ email, no password, no account portal.
PIN-recovery form for that account ("Already taken — sign in instead, or pick another 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"** 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. 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 - **PIN reset by Host / Admin.** If a guest loses their PIN and `localStorage` is
gone everywhere, a Host (for guests) or Admin (for hosts and guests) can hit a 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 **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 bcrypt stored, and the plaintext is shown **once** in a modal to the requesting
@@ -153,9 +153,13 @@ email, no password, no account portal.
Admins can. Admins can.
- **Roles.** `guest` (default), `host`, `admin`. The Admin role is seeded from the - **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 `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 JWT, 1-day expiry, in `sessionStorage`). Hosts are guests promoted by a host or an admin.
may also demote other Hosts to guests** (planned) — but never themselves, to avoid **Only an Admin may change a Host's role** — a plain Host may promote guests but may not
locking the event out of moderation. Admins can demote anyone except admins. demote a peer Host, because demoting one would then let them ban or PIN-reset (→
`/recover` takeover) that ex-peer, the guards keying off the target's *current* role (F1).
Nobody may change their own role, so the event can't be locked out of moderation, and
Admins are un-demotable and un-bannable. Enforced in `handlers::host::set_role`; the
button is hidden for non-admin Hosts.
### 2.2 Posting pipeline ### 2.2 Posting pipeline
@@ -188,8 +192,9 @@ The upload pipeline is built for flaky mobile networks:
the loaded uploads, so typing never hits the server. the loaded uploads, so typing never hits the server.
- **Filter chips** — multiple hashtags combine with OR; multiple uploaders combine with OR; - **Filter chips** — multiple hashtags combine with OR; multiple uploaders combine with OR;
hashtag + uploader combine with AND. Matches the redesign concept exactly. hashtag + uploader combine with AND. Matches the redesign concept exactly.
- **Lightbox** — fullscreen view, swipe navigates the *filtered* set, with embedded - **Lightbox** — fullscreen view with embedded like/comment UI. Navigation across the
like/comment UI. *filtered* set is available three ways: on-screen prev/next controls, the ← / → arrow
keys, and a left/right swipe on touch.
- **Real-time** — SSE delivers `new-upload`, `upload-processed`, `like-update`, - **Real-time** — SSE delivers `new-upload`, `upload-processed`, `like-update`,
`new-comment`, `upload-deleted`, `event-closed`/`event-opened`, `export-progress`, `new-comment`, `upload-deleted`, `event-closed`/`event-opened`, `export-progress`,
`export-available`. Client pauses SSE on `visibilitychange: hidden` and reopens on visible. `export-available`. Client pauses SSE on `visibilitychange: hidden` and reopens on visible.
@@ -197,15 +202,18 @@ The upload pipeline is built for flaky mobile networks:
### 2.4 Host / Admin tooling ### 2.4 Host / Admin tooling
- **Host dashboard** — three collapsible sections: Stats, Event-Einstellungen, - **Host dashboard** — three collapsible sections: Stats, Event-Einstellungen,
Nutzerverwaltung. Ban modal asks explicitly whether to hide the user's existing uploads Nutzerverwaltung. The ban modal is a plain confirm: banning **always hides** the user's
from the public feed. Promote/demote, lock/unlock, release-gallery are one-tap. existing uploads, there is no keep-visible option. Promote/demote/unban/release are each
- **Admin dashboard** — same dashboard plus three more inner tabs (Stats, Config, Export, behind a confirmation sheet that spells out the consequence, not one-tap.
- **Admin dashboard** — same user list plus four inner tabs (Stats, Config, Export,
Nutzer). Config form covers per-file limits, rate limits, quota tolerance, estimated 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 guest count and the colour theme — all stored in the `config` table and read on
each request, so changes take effect without a restart. Disk widget pulls from the each request, so changes take effect without a restart. Disk widget pulls from the
`sysinfo` crate live. `sysinfo` crate live. The Export tab mirrors the host dashboard's release controls:
the release button is disabled once released, the live keepsake status (progress /
ready / failure reason) is shown, and a rebuild is offered as the recovery path.
### 2.5 Data mode (planned) ### 2.5 Data mode
Each device picks a **data mode** in My Account; the setting lives in `localStorage` so a 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. guest can be on Saver on their phone and Original on their laptop.
@@ -218,7 +226,7 @@ guest can be on Saver on their phone and Original on their laptop.
Applies uniformly to the live app's feed/lightbox **and** the diashow. The viewer (offline 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. HTML export) is unaffected — it's already a snapshot of pre-bundled media variants.
### 2.6 Rate limits and quotas — toggleable (planned) ### 2.6 Rate limits and quotas — toggleable
The Admin Config tab gains explicit on/off toggles in addition to the numeric inputs: The Admin Config tab gains explicit on/off toggles in addition to the numeric inputs:
@@ -230,13 +238,14 @@ The Admin Config tab gains explicit on/off toggles in addition to the numeric in
- **Per-area quota switch.** Storage-bytes quota and upload-count quota can be disabled - **Per-area quota switch.** Storage-bytes quota and upload-count quota can be disabled
independently. independently.
When a feature is toggled off, the relevant UI in the guest-facing app should adapt: e.g. When a feature is toggled off, the dependent UI adapts: the "Speicher: X / Y" widget hides
the "Du hast X von Y MB genutzt" widget hides itself when storage quota is disabled. The itself when the storage quota is disabled. The quota estimate is computed from the same
quota estimate is computed from the same formula the server uses formula the server uses (`(free_disk × tolerance) / max(active_uploaders, 1)`) and is
(`(free_disk × tolerance) / max(active_uploaders, 1)`) — surfaced in My Account *and* on surfaced in My Account *and* on the upload preview screen — but **only to hosts and
the upload preview screen so guests know before they pick files. admins**. Guests never see it: the number moves as other people upload and as disk frees
up, which reads as a broken or unfair limit to someone who can't see why.
### 2.7 Privacy note (Datenschutzhinweis, planned) ### 2.7 Privacy note (Datenschutzhinweis)
Admin sets a free-text **Datenschutzhinweis** during instance setup (Admin Dashboard → 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 Config). It's stored as a single config key (plain text, whitespace and newlines

View File

@@ -19,8 +19,9 @@ can do what" overview, see [FEATURES.md](FEATURES.md). For manual QA, see
this PIN is the only way to sign in on another device. PIN is also written to this PIN is the only way to sign in on another device. PIN is also written to
`localStorage`. `localStorage`.
6. Guest taps **Weiter zur Galerie** → lands in the feed (`/feed`). 6. Guest taps **Weiter zur Galerie** → lands in the feed (`/feed`).
7. The **first-visit onboarding overlay** appears: dismissible steps (welcome, upload, 7. The **first-visit onboarding overlay** appears: six dismissible steps (welcome, upload,
hashtags, PIN, and a brief pointer to the **Datenschutzhinweis** in My Account). hashtags, long-press for more actions, hell/dunkel design pick, and PIN — the PIN step
also points at the **Datenschutzhinweis** in My Account when the admin has set one).
`localStorage('eventsnap_guide_seen') = 'true'` after dismiss. `localStorage('eventsnap_guide_seen') = 'true'` after dismiss.
8. Guest sees the bottom nav: **🏠 Feed · [📷+ FAB] · 👤 Account**. 8. Guest sees the bottom nav: **🏠 Feed · [📷+ FAB] · 👤 Account**.
@@ -41,7 +42,7 @@ can do what" overview, see [FEATURES.md](FEATURES.md). For manual QA, see
5. Wrong PIN: up to 3 attempts. After the third, the account is locked for 15 minutes 5. Wrong PIN: up to 3 attempts. After the third, the account is locked for 15 minutes
(`pin_locked_until` is set; further attempts return HTTP 429 with a localized message). (`pin_locked_until` is set; further attempts return HTTP 429 with a localized message).
## 4. PIN forgotten — Host or Admin resets it (planned) ## 4. PIN forgotten — Host or Admin resets it
The PIN is visible in **My Account** as long as `localStorage` is intact on at least one The PIN is visible in **My Account** as long as `localStorage` is intact on at least one
of the user's devices. If lost everywhere, the user asks a Host (or Admin) for a reset. of the user's devices. If lost everywhere, the user asks a Host (or Admin) for a reset.
@@ -126,14 +127,23 @@ the Host can clean up later).
3. **Event settings** — toggle to lock new uploads (likes / comments / browsing stay open; 3. **Event settings** — toggle to lock new uploads (likes / comments / browsing stay open;
broadcasts `event-closed` SSE so all clients show a "uploads are locked" banner). broadcasts `event-closed` SSE so all clients show a "uploads are locked" banner).
4. **Galerie freigeben** — releases the export. Enqueues two export jobs (ZIP + HTML 4. **Galerie freigeben** — releases the export. Enqueues two export jobs (ZIP + HTML
viewer). Progress is visible in the Admin dashboard's Export tab; SSE viewer). Progress is visible on the Host dashboard *and* in the Admin dashboard's Export
`export-progress` keeps it live; `export-available` notifies all guests when ready. tab; SSE `export-progress` keeps it live; `export-available` notifies all guests when
ready. Once released the button is disabled and reads *"Galerie bereits freigegeben"*
a second release is a 409. If a keepsake half fails, both dashboards show the reason and
offer **Erneut versuchen** (rebuild); a ready keepsake can also be rebuilt behind a
confirm, during which guest downloads are briefly unavailable.
5. **Nutzerverwaltung** — search users; per-user controls: 5. **Nutzerverwaltung** — search users; per-user controls:
- **Sperren** opens a confirmation modal. Banning **always hides** the user's existing - **Sperren** opens a confirmation modal. Banning **always hides** the user's existing
uploads (a banned user's content is "gone" everywhere) — there is no opt-out. Submitting uploads (a banned user's content is "gone" everywhere) — there is no opt-out. Submitting
calls `POST /host/users/{id}/ban` (no body). calls `POST /host/users/{id}/ban` (no body).
- **Entsperren** lifts the ban. Same authority boundary as ban (below): a plain Host may - **Entsperren** lifts the ban — and does two further things the confirm sheet now spells
only unban Guests; only an Admin may unban a Host. out, because both are visible to the whole party: it clears `uploads_hidden`, so **all**
of that user's previously hidden uploads reappear in the gallery, the diashow and the
export; and it invalidates and rebuilds a released keepsake (`invalidate_and_arm`), so
every guest's download is unavailable for as long as that takes. A ban does the mirror
image of both. Same authority boundary as ban: a plain Host may only unban Guests; only
an Admin may unban a Host.
- **Host** promotes a guest to host (Hosts and Admins may do this). - **Host** promotes a guest to host (Hosts and Admins may do this).
- **Degradieren** — demote a Host back to guest. **Only an Admin may change a Host's - **Degradieren** — demote a Host back to guest. **Only an Admin may change a Host's
role.** A plain Host may *not* demote a peer Host: doing so would let them then ban or role.** A plain Host may *not* demote a peer Host: doing so would let them then ban or
@@ -181,14 +191,18 @@ the Host can clean up later).
- **Stats**: live counts and disk-usage widget (via `sysinfo`). - **Stats**: live counts and disk-usage widget (via `sysinfo`).
- **Config**: per-file limits (image MB / video MB), rate limits (upload / feed / - **Config**: per-file limits (image MB / video MB), rate limits (upload / feed /
export), quota tolerance, estimated guest count, compression-worker concurrency, export), quota tolerance, estimated guest count, compression-worker concurrency,
plus the **Datenschutzhinweis** free-text editor and **on/off toggles** for the rate plus the colour-theme picker, the **Datenschutzhinweis** free-text editor and
limiters and quotas (planned — see §16). Whitelist on the server side rejects **on/off toggles** for the rate limiters and quotas (see §18). Whitelist on the
unknown keys. Values are read from the `config` table on each request — no restart server side rejects unknown keys. Values are read from the `config` table on each
needed. request — no restart needed.
- **Export**: list of past export jobs with status badges (pending / running / done / - **Export**: the gallery-release control plus the list of export jobs with status
failed) and progress bars; refresh button re-polls. badges (pending / running / done / failed) and progress bars; refresh button
- **Nutzer**: same user list as Host, with the additional Demote action and (planned) re-polls. The release control mirrors the Host dashboard exactly — disabled once
PIN-reset on host rows. released (a second tap would only 409), live keepsake status with progress and the
failure reason, and a **Neu erstellen / Erneut versuchen** rebuild as the recovery
path for a failed or stale keepsake.
- **Nutzer**: same user list as Host, with the additional Demote action (admin-only,
see §9) and PIN-reset on host rows.
## 12. Releasing the export and downloading ## 12. Releasing the export and downloading
@@ -224,11 +238,14 @@ the Host can clean up later).
buttons. Tapping the HTML download first shows an in-app guide modal explaining: buttons. Tapping the HTML download first shows an in-app guide modal explaining:
"Entpacke die ZIP, öffne `index.html`". Tapping **Herunterladen** triggers the "Entpacke die ZIP, öffne `index.html`". Tapping **Herunterladen** triggers the
browser download. browser download.
7. Downloads are rate-limited per IP (default 3 / day). 7. Downloads are rate-limited **per user** (default 3 / day), keyed on the user id in
`enforce_export_rate` — deliberately not per IP. At a venue where a hundred guests share
one NAT'd wifi, a per-IP budget would be exhausted by the third person to tap
**Herunterladen** and lock everyone else out of their own keepsake.
## 13. Diashow (planned) ## 13. Diashow
See [CONCEPT_DIASHOW.md](CONCEPT_DIASHOW.md). Summary of the planned flow: See [CONCEPT_DIASHOW.md](CONCEPT_DIASHOW.md). Summary of the flow:
1. User taps a **Diashow / Präsentation** action (feed header on tablet/desktop, Account 1. User taps a **Diashow / Präsentation** action (feed header on tablet/desktop, Account
on mobile). on mobile).
@@ -240,7 +257,7 @@ See [CONCEPT_DIASHOW.md](CONCEPT_DIASHOW.md). Summary of the planned flow:
immediately. immediately.
6. Tap or Escape reveals an overlay (pause, dwell selector, exit). 6. Tap or Escape reveals an overlay (pause, dwell selector, exit).
## 14. Picking a data mode (planned) ## 14. Picking a data mode
1. Guest opens **My Account** → scrolls to **Datennutzung**. 1. Guest opens **My Account** → scrolls to **Datennutzung**.
2. Two options: **Datensparer (empfohlen)** and **Original**. Saver is the default. 2. Two options: **Datensparer (empfohlen)** and **Original**. Saver is the default.
@@ -252,14 +269,24 @@ See [CONCEPT_DIASHOW.md](CONCEPT_DIASHOW.md). Summary of the planned flow:
5. The viewer (offline HTML export) is unaffected — it already ships with its own pre- 5. The viewer (offline HTML export) is unaffected — it already ships with its own pre-
bundled `_thumb` / `_full` variants. bundled `_thumb` / `_full` variants.
## 15. Leaving an event ## 15. Signing out
1. User opens **My Account** → taps **🚪 Event verlassen**. The wording matters here: nothing is *left* and nothing is deleted. The account, the PIN
2. Bottom-sheet confirmation: "Event verlassen?" with **Abmelden** and **Bleiben**. and every uploaded photo survive — the user signs back in with name + PIN whenever they
3. Confirming calls `DELETE /api/v1/session` (invalidates the session row), clears the JWT like. ("Event verlassen" read as leaving for good, which the confirmation sheet then
and PIN from `localStorage`, and redirects to the join page. contradicted by saying you can come back.)
## 16. Reading the Datenschutzhinweis (planned) 1. User opens **My Account** → Konto section. Two separate actions:
- **Abmelden** — this device only (`DELETE /api/v1/session`).
- **Auf allen Geräten abmelden** — every session of theirs (`DELETE /api/v1/sessions`),
for a lost or borrowed phone.
2. Bottom-sheet confirmation naming which of the two it is, with **Abmelden** / **Abbrechen**.
3. Confirming invalidates the session row(s), wipes the local JWT, PIN and the IndexedDB
upload queue (so the next guest on a shared phone doesn't inherit pending uploads), and
redirects to the join page. The session-delete call is best-effort: the token is gone
locally either way, so a network failure never traps the user on the page.
## 16. Reading the Datenschutzhinweis
1. User opens **My Account** → scrolls to **Datenschutzhinweis**. 1. User opens **My Account** → scrolls to **Datenschutzhinweis**.
2. The note is rendered inside a preformatted block (`<pre>`-style: monospace, whitespace 2. The note is rendered inside a preformatted block (`<pre>`-style: monospace, whitespace
@@ -270,11 +297,15 @@ See [CONCEPT_DIASHOW.md](CONCEPT_DIASHOW.md). Summary of the planned flow:
4. Admin sets / edits the note in **Admin Dashboard → Config → Datenschutzhinweis**: a 4. Admin sets / edits the note in **Admin Dashboard → Config → Datenschutzhinweis**: a
tall textarea with a save button. Saved to a single `config` key. tall textarea with a save button. Saved to a single `config` key.
## 17. Mobile-first gestures (planned) ## 17. Mobile-first gestures (partly shipped)
EventSnap's UI is mobile-first; gestures replace explicit buttons where they're more EventSnap's UI is mobile-first; gestures replace explicit buttons where they're more
ergonomic. Buttons are always present as fallback for desktop and accessibility. ergonomic. Buttons are always present as fallback for desktop and accessibility.
Shipped today: long-press context sheets on posts and comments, and lightbox navigation
(on-screen prev/next controls, ← / → arrow keys, and left/right swipe). The remaining rows
below are still planned.
| Gesture | Action | | Gesture | Action |
|-------------------------------------------|-------------------------------------------------------| |-------------------------------------------|-------------------------------------------------------|
| Long-press on a post (own) | Bottom sheet → Löschen, Original anzeigen, Teilen | | Long-press on a post (own) | Bottom sheet → Löschen, Original anzeigen, Teilen |
@@ -294,7 +325,7 @@ Inspiration: Instagram (double-tap heart, swipe stories), WhatsApp (long-press f
context), Telegram (swipe-to-reply on messages — could inform comment threads if those context), Telegram (swipe-to-reply on messages — could inform comment threads if those
land). land).
## 18. Admin toggles a rate limit or quota off (planned) ## 18. Admin toggles a rate limit or quota off
1. Admin opens **Admin Dashboard → Config**. 1. Admin opens **Admin Dashboard → Config**.
2. **Rate-Limits** section: a master switch and per-endpoint switches (upload / feed / 2. **Rate-Limits** section: a master switch and per-endpoint switches (upload / feed /
@@ -305,8 +336,9 @@ land).
limiter entirely. limiter entirely.
5. **Quoten** section mirrors the pattern: master toggle plus per-area toggles (storage 5. **Quoten** section mirrors the pattern: master toggle plus per-area toggles (storage
bytes / upload count). bytes / upload count).
6. When the storage-quota toggle is off, the **"X von Y MB genutzt"** widget in the 6. When the storage-quota toggle is off, the **"Speicher: X / Y"** widget in My Account and
guest's My Account and upload screen hides itself (no quota → no number to show). on the upload screen hides itself (no quota → no number to show). That widget is
staff-only in any case — guests never see it.
Suggested defaults at deploy time: all toggles **on**, sensible numeric limits. Suggested defaults at deploy time: all toggles **on**, sensible numeric limits.
Toggling off is the explicit escape hatch for testing or trusted internal events. Toggling off is the explicit escape hatch for testing or trusted internal events.