CR1: the lightbox comment button POSTed to /upload/{id}/comment (singular);
no such route exists, so every UI-submitted comment 404'd and was lost.
Fixed to /comments (plural). The prior e2e passed because its seed helper
POSTs the API directly — added comment-ui.spec.ts which drives the real
component so this can't regress silently again.
CR2: export archives (Gallery.zip / Memories.zip / HTML viewer) were written
under media_path, which is a public ServeDir — so GET /media/exports/
Gallery.zip served the entire event (every photo, caption, comment,
uploader name) to any anonymous visitor at a guessable URL, bypassing the
ticket + release gate. Moved exports to a separate EXPORT_PATH (=/exports)
on its own volume (Dockerfile chown, compose volume, gated handler reads
the new path). export-leak.spec.ts asserts /media/exports/Gallery.zip → 404.
Riders (git can't split hunks): LightboxModal also gains H3 live-eviction on
comment-deleted/upload-deleted; config.rs also wires compression_concurrency
from boot config (medium).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
H1: auth extractor re-reads the live user row — trusts the DB role (not the
JWT claim) and rejects banned users mid-session. e2e proves a demoted
host loses powers and a banned host is locked out and can't self-unban.
H2: client_ip takes the right-most XFF hop (the one Caddy appends); spoofed
left-most entries are ignored, restoring IP-based throttles.
H3: Caddy excludes /api/v1/stream from `encode` so SSE isn't buffered.
H4: upload — quota increment + row insert + hashtag links now one txn.
H5: feed keyset pagination tiebroken on (created_at, id) + composite index
(migration 010); feed_delta bounded with LIMIT.
H7: LightboxModal keys its comment load off upload.id, ending the
SSE-driven refetch storm.
H8: CSP via SvelteKit kit.csp (svelte.config.js) hardens the localStorage
token model against injection.
Migration 010 also adds the latent comment/comment_hashtag indexes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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) <noreply@anthropic.com>
- Cursor-based feed endpoint using v_feed view with hashtag filtering
- Like toggle (INSERT ON CONFLICT), comments CRUD
- Feed delta endpoint for SSE-driven incremental updates
- SSE client with Page Visibility API (pause/reconnect)
- Responsive photo/video grid with infinite scroll
- Hashtag filter chips, lightbox modal with comments
- Media file serving via tower-http ServeDir
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>