diff --git a/PROJECT.md b/PROJECT.md
index 94c189d..1e009a8 100644
--- a/PROJECT.md
+++ b/PROJECT.md
@@ -42,7 +42,7 @@ A guest scans the QR code on their way in, types their name, and is immediately
Mobile-first Progressive Web App (PWA) — accessible via browser, no app store required.
### Status
-Idea / Planning phase. Greenfield personal project.
+Implementation in progress (~v0.16). Core flows + new features all wired: auth, feed, upload, host/admin dashboards, ZIP + HTML-viewer export, SSE with delta-fetch on reconnect, toggleable rate limits + quotas with live per-user estimate, host PIN reset with one-time modal, data-mode (Saver/Original), Datenschutzhinweis, mobile gestures (long-press context sheet, double-tap to like), and the live Diashow with pluggable transitions. Open items: low-disk alert, event banner UI, chunked resumable upload for very large videos. See [FEATURES.md](docs/FEATURES.md) for the capability matrix.
---
@@ -208,9 +208,9 @@ Personal / private use. One event at a time. Up to ~100 users uploading ~1,000 f
│ Axum HTTP Server (Rust — Single Binary) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
-│ │ REST API │ │ SSE Engine │ │ Static File Server │ │
-│ │ /api/v1/* │ │ /api/v1/ │ │ (SvelteKit build │ │
-│ │ │ │ stream │ │ output, embedded) │ │
+│ │ REST API │ │ SSE Engine │ │ Media Static Server │ │
+│ │ /api/v1/* │ │ /api/v1/ │ │ /media/* (originals, │ │
+│ │ │ │ stream │ │ previews, thumbnails) │ │
│ └──────┬──────┘ └──────┬───────┘ └────────────────────────┘ │
│ │ │ │
│ ┌──────▼──────────────────────┐ ┌──────────────────────────┐ │
@@ -245,36 +245,14 @@ Personal / private use. One event at a time. Up to ~100 users uploading ~1,000 f
### Docker Compose Stack
+Four services: Postgres, the Rust API (`app`), the SvelteKit Node server (`frontend`), and Caddy. Caddy routes `/api/*` and `/media/*` to the Rust binary and everything else to the SvelteKit server. See [docker-compose.yml](docker-compose.yml) for the authoritative definition.
+
```yaml
services:
- app:
- build: ./backend # Multi-stage Rust Dockerfile
- env_file: .env
- depends_on: [db]
- volumes:
- - media_data:/media
- restart: unless-stopped
-
- db:
- image: postgres:16-alpine
- env_file: .env
- volumes:
- - postgres_data:/var/lib/postgresql/data
- restart: unless-stopped
-
- caddy:
- image: caddy:2-alpine
- ports: ["80:80", "443:443"]
- volumes:
- - ./Caddyfile:/etc/caddy/Caddyfile:ro
- - caddy_data:/data
- depends_on: [app]
- restart: unless-stopped
-
-volumes:
- postgres_data:
- media_data:
- caddy_data:
+ db: # postgres:16-alpine, persisted in postgres_data volume
+ app: # ./backend — Rust API on :3000, mounts media_data:/media
+ frontend: # ./frontend — SvelteKit (adapter-node) on :3001
+ caddy: # caddy:2-alpine — terminates TLS on :80/:443, proxies app + frontend
```
### Caddyfile
@@ -345,11 +323,14 @@ COMPRESSION_WORKER_CONCURRENCY=2
|-------|---------|---------|
| `new-upload` | `{ id, preview_url, uploader, caption, created_at }` | Upload processing complete |
| `new-comment` | `{ id, upload_id, body, uploader, created_at }` | Comment posted |
-| `new-like` | `{ upload_id, like_count }` | Like toggled |
+| `like-update` | `{ upload_id, like_count }` | Like toggled |
| `upload-deleted` | `{ upload_id }` | Upload deleted |
| `event-closed` | `{}` | Host locks uploads |
| `event-opened` | `{}` | Host unlocks uploads |
| `export-available` | `{ types: ["zip","html"] }` | Export generation complete |
+| `upload-processed` | `{ upload_id, preview_url, thumbnail_url }` | Server-side compression / preview generation finished |
+| `upload-error` | `{ upload_id, message }` | Compression / preview generation failed |
+| `export-progress` | `{ type, progress_pct }` | Periodic progress update from an export job |
**Client SSE lifecycle:** `visibilitychange: hidden` → close connection · `visible` → reconnect + delta-fetch via `GET /api/v1/feed/delta?since=`
@@ -372,7 +353,7 @@ COMPRESSION_WORKER_CONCURRENCY=2
| Real-Time | Axum SSE + `tokio::sync::broadcast` | Native, lightweight, perfect for fan-out at this scale |
| ZIP Export | `async-zip` crate | Streaming ZIP generation without buffering the full archive in RAM |
| HTML Export | `minijinja` (Rust templating) | Generates `Memories.html` as a single self-contained file |
-| Rate Limiting | `tower-governor` | Token-bucket per IP / per user; config from DB; hot-reloadable |
+| Rate Limiting | Custom in-memory sliding-window limiter ([services/rate_limiter.rs](backend/src/services/rate_limiter.rs)) | Per IP / per user; limits read from `config` DB table on each request; hot-reloadable without restart |
| Reverse Proxy | Caddy 2 | Automatic HTTPS via Let's Encrypt; zero certificate management |
| Containerisation | Docker + Docker Compose | Full stack in one file; `.env` for all config; single-command deploy |
| Infrastructure | Hetzner CX33 (4 vCPU, 8 GB RAM, 80 GB SSD, 20 TB traffic) | Well-sized; 20 TB/month means post-event bulk downloads are no issue |
@@ -417,9 +398,9 @@ No paid third-party services required.
| Role | Permissions |
|------|------------|
-| Guest | Upload (within quota), caption/hashtag, like, comment, delete own content, view feed, download export (after release) |
-| Host | All guest permissions + ban/unban users (with upload visibility prompt), delete any content, promote guests to Host, lock/unlock uploads, release gallery export |
-| Admin | All Host permissions + configure storage/file/rate limits, quota tolerance, view disk usage, manage app config, trigger export generation |
+| Guest | Upload (within quota), caption/hashtag, like, comment, delete own content, view feed, download export (after release), pick data mode, read privacy note |
+| Host | All guest permissions + ban/unban users (with upload visibility prompt), delete any content, promote guests to Host, demote *other* Hosts to guest (never self), reset guest PINs (planned), lock/unlock uploads, release gallery export |
+| Admin | All Host permissions + reset any non-admin PIN, configure storage/file/rate limits with on/off toggles, edit quota tolerance and per-area quota toggles, edit the Datenschutzhinweis, view disk usage, manage app config, trigger export generation |
| Banned Guest | View feed only — cannot upload, like, comment, or export |
### Compliance
@@ -473,37 +454,33 @@ Full-quality originals only. File naming: `{date}_{time}_{username}_{original_fi
### Export Type 2: HTML Offline Viewer (`Memories.zip`)
+The HTML export is a **pre-built SvelteKit static app** (`adapter-static`, `ssr=false`) shipped together with the event data. It is a non-interactive, read-only clone of the live feed — same components, same Tailwind tokens, same look — minus auth, upload, comment, and any dashboards. Full design rationale in [docs/CONCEPT_HTML_VIEWER.md](docs/CONCEPT_HTML_VIEWER.md).
+
```
Memories/
- Memories.html ← single entry point (all CSS + JS inlined; no external deps)
- README.txt ← plain-text setup guide (in German, as the UI language)
- Photos/ ...
- Videos/ ...
+ index.html ← entry point; open this in any browser
+ _app/
+ immutable/... ← hashed JS/CSS bundles (viewer SPA)
+ data.json ← event metadata, posts, comments, likes, hashtags
+ media/
+ {id}_thumb.jpg ← grid thumbnails (≈400 px wide)
+ {id}_full.jpg/.mp4 ← full-size media for the lightbox
```
-**Fully self-contained / true offline:** `Memories.html` is a single file with all CSS and JS inlined as `
+
{upload.caption}
+{upload.caption}
{/if}Noch keine Kommentare.
+Noch keine Kommentare.
{:else}