From 9a0ceeced72f50d5133bb2a9faf8d00fd93272e7 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 16 May 2026 14:31:06 +0200 Subject: [PATCH 1/7] docs: realign blueprint with shipped state + add feature/journey/ideas docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- PROJECT.md | 122 +++++++------- README.md | 44 +++-- TEST_GUIDE.md | 52 +++--- backend/migrations/README.md | 28 ++++ docs/CONCEPT_DIASHOW.md | 196 ++++++++++++++++++++++ docs/CONCEPT_HTML_VIEWER.md | 10 ++ docs/CONCEPT_MOBILE_UI.md | 57 ++++++- docs/FEATURES.md | 313 +++++++++++++++++++++++++++++++++++ docs/IDEAS.md | 199 ++++++++++++++++++++++ docs/USER_JOURNEYS.md | 292 ++++++++++++++++++++++++++++++++ frontend/src/lib/README.md | 34 ++++ 11 files changed, 1241 insertions(+), 106 deletions(-) create mode 100644 backend/migrations/README.md create mode 100644 docs/CONCEPT_DIASHOW.md create mode 100644 docs/FEATURES.md create mode 100644 docs/IDEAS.md create mode 100644 docs/USER_JOURNEYS.md create mode 100644 frontend/src/lib/README.md 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 ` diff --git a/frontend/src/lib/diashow/transitions/index.ts b/frontend/src/lib/diashow/transitions/index.ts new file mode 100644 index 0000000..026426c --- /dev/null +++ b/frontend/src/lib/diashow/transitions/index.ts @@ -0,0 +1,47 @@ +// Pluggable transition registry. The diashow consults this list to populate its +// settings popover and to render the current transition. +// +// Adding a new animation: +// 1. Drop a Svelte file alongside crossfade.svelte / kenburns.svelte. +// 2. Add one entry to `transitions` below. +// 3. Rebuild — the popover updates automatically. +// +// This is the extensibility principle from docs/FEATURES.md §2.9 made concrete: +// no diashow code needs to change to add an animation. + +import type { Component } from 'svelte'; +import Crossfade from './crossfade.svelte'; +import KenBurns from './kenburns.svelte'; + +export interface SlideTransition { + id: string; + label: string; + defaultDurationMs: number; + component: Component; +} + +/** Props every transition Svelte component receives. */ +export interface TransitionProps { + src: string; + isVideo: boolean; + durationMs: number; +} + +export const transitions: SlideTransition[] = [ + { + id: 'crossfade', + label: 'Überblendung', + defaultDurationMs: 400, + component: Crossfade as unknown as Component + }, + { + id: 'kenburns', + label: 'Ken Burns', + defaultDurationMs: 600, + component: KenBurns as unknown as Component + } +]; + +export function findTransition(id: string): SlideTransition { + return transitions.find((t) => t.id === id) ?? transitions[0]; +} diff --git a/frontend/src/lib/diashow/transitions/kenburns.svelte b/frontend/src/lib/diashow/transitions/kenburns.svelte new file mode 100644 index 0000000..0050d71 --- /dev/null +++ b/frontend/src/lib/diashow/transitions/kenburns.svelte @@ -0,0 +1,51 @@ + + +
+ {#if isVideo} + + + {:else} + + {/if} +
+ + diff --git a/frontend/src/lib/diashow/wakelock.ts b/frontend/src/lib/diashow/wakelock.ts new file mode 100644 index 0000000..fcc2bac --- /dev/null +++ b/frontend/src/lib/diashow/wakelock.ts @@ -0,0 +1,54 @@ +// Thin wrapper around the Screen Wake Lock API. Held by the diashow page so a phone +// driving a projector doesn't sleep. No-op on unsupported browsers (Firefox, older +// Safari). +// +// Wake locks die when the document goes hidden; the page re-acquires on visible to +// keep the screen on across short interruptions. + +interface SentinelLike { + release: () => Promise; +} + +let sentinel: SentinelLike | null = null; +let visibilityHandler: (() => void) | null = null; + +export async function acquireWakeLock(): Promise { + const wakeLock = (navigator as Navigator & { wakeLock?: { request: (t: string) => Promise } }).wakeLock; + if (!wakeLock) return; + try { + sentinel = await wakeLock.request('screen'); + } catch { + // User denied, or already released — nothing useful to do. + sentinel = null; + } + + // Re-acquire when the page becomes visible again (the OS releases the lock + // while the tab is hidden). + if (!visibilityHandler) { + visibilityHandler = async () => { + if (document.visibilityState === 'visible' && sentinel === null) { + try { + sentinel = await wakeLock.request('screen'); + } catch { + sentinel = null; + } + } + }; + document.addEventListener('visibilitychange', visibilityHandler); + } +} + +export async function releaseWakeLock(): Promise { + if (sentinel) { + try { + await sentinel.release(); + } catch { + // ignore — release after release is fine + } + sentinel = null; + } + if (visibilityHandler) { + document.removeEventListener('visibilitychange', visibilityHandler); + visibilityHandler = null; + } +} diff --git a/frontend/src/routes/diashow/+page.svelte b/frontend/src/routes/diashow/+page.svelte new file mode 100644 index 0000000..e8d5bd6 --- /dev/null +++ b/frontend/src/routes/diashow/+page.svelte @@ -0,0 +1,223 @@ + + + + + From e619a3bd64430d2dde486ec9d180790467d57eef Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 16 May 2026 14:33:30 +0200 Subject: [PATCH 6/7] feat(ui): v0.16 features + dark mode across every page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires up everything from the previous commits into actual UI surfaces, and applies Tailwind dark: variants throughout. All pages now support the 'system' / 'light' / 'dark' preference set in the onboarding step or in Mein Konto → Design. Layout & nav: - routes/+layout.svelte: initTheme(), global pin-reset SSE handler that filters by user_id and calls clearPin(), one-shot /me/context fetch on boot to hydrate privacyNote + quota. - components/BottomNav.svelte: dark variants on the frosted-glass bar. - components/UploadSheet.svelte: dark variants on backdrop, sheet, source buttons. - components/OnboardingGuide.svelte: new "Helles oder dunkles Design?" step (3-option custom-radio grid), reactive currentStep with proper type narrowing, dark variants throughout. Privacy-note nudge appears on the PIN step only when one is configured. Feed: - routes/feed/+page.svelte: diashow entry icon (tablet/desktop only), long-press → ContextSheet (Löschen for own posts, Original anzeigen for all), upload-deleted + feed-delta SSE handlers, dark variants on header, search, autocomplete, filter chips, empty states. - components/FeedListCard.svelte: long-press wireup, double-tap-to-like, data-mode-aware mediaSrc via pickMediaUrl, kebab fallback for desktop, isOwn prop, dark variants. - components/FeedGrid.svelte: long-press wireup, dark variants. - components/LightboxModal.svelte: data-mode-aware src, double-tap heart burst, dark variants on card / comments / input. - components/HashtagChips.svelte: dark variants. Account: - routes/account/+page.svelte: theme picker (3-button radio grid), data mode picker (with confirm sheet for Original), live quota widget, preformatted Datenschutzhinweis block, diashow tile (mobile only), pin now sourced from the $currentPin store so a global pin-reset clears it live, clearQueue() on explicit logout, dark variants across every card + both bottom sheets. Upload: - routes/upload/+page.svelte: per-user quota progress bar above the submit button, dark variants. Host & Admin: - routes/host/+page.svelte: PIN-reset confirm + one-time PIN modal, hosts may demote other hosts, canResetPinFor() helper, dark variants on all cards, modals, stats, toast. - routes/admin/+page.svelte: Config form rebuilt as CONFIG_GROUPS with per-field kind (number / bool / text), renders toggles for the rate-limit + quota switches and a textarea for the privacy_note; Nutzer tab gains PIN reset + hosts-may-demote-hosts wiring; same one-time PIN modal; dark variants everywhere. - routes/admin/login/+page.svelte: dark variants. Join / Recover / Export: - routes/join/+page.svelte: rename inline link to "Ich habe bereits einen Account", dark variants. - routes/recover/+page.svelte: dark variants. - routes/export/+page.svelte: dark variants on status cards + HTML guide modal. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/lib/components/BottomNav.svelte | 6 +- frontend/src/lib/components/FeedGrid.svelte | 26 +- .../src/lib/components/FeedListCard.svelte | 93 +++-- .../src/lib/components/HashtagChips.svelte | 4 +- .../src/lib/components/LightboxModal.svelte | 103 +++-- .../src/lib/components/OnboardingGuide.svelte | 97 ++++- .../src/lib/components/UploadSheet.svelte | 22 +- frontend/src/routes/+layout.svelte | 47 ++- frontend/src/routes/account/+page.svelte | 336 ++++++++++++--- frontend/src/routes/admin/+page.svelte | 388 +++++++++++++----- frontend/src/routes/admin/login/+page.svelte | 16 +- frontend/src/routes/export/+page.svelte | 60 +-- frontend/src/routes/feed/+page.svelte | 175 ++++++-- frontend/src/routes/host/+page.svelte | 205 ++++++--- frontend/src/routes/join/+page.svelte | 47 ++- frontend/src/routes/recover/+page.svelte | 18 +- frontend/src/routes/upload/+page.svelte | 90 +++- 17 files changed, 1295 insertions(+), 438 deletions(-) diff --git a/frontend/src/lib/components/BottomNav.svelte b/frontend/src/lib/components/BottomNav.svelte index 6af5afd..0593bf9 100644 --- a/frontend/src/lib/components/BottomNav.svelte +++ b/frontend/src/lib/components/BottomNav.svelte @@ -9,7 +9,7 @@