Commit Graph

22 Commits

Author SHA1 Message Date
fabi
1485df5469 fix(auth): bind the role store to the identity, not to the tab
The role store I added in the moderation work is a module-level singleton seeded
ONCE at import. `goto()` is a client-side navigation, so leaving and re-joining in
the same tab re-imports no module and re-runs no onMount — the previous user's
role simply stayed resident. Nothing reset it: not join, recover, admin login,
"Event verlassen", `clearAuth`, nor the api.ts 401 auto-clear.

So a host who left, followed by a guest joining on the same phone, left that guest
with `isStaff === true` and a "🚫 Beitrag entfernen" action on other people's
photos. The backend 403s the delete, so this was a false affordance rather than a
privilege escalation — but `/feed` never fetched `/me/context`, so unlike every
other route it never self-corrected either. It survived until a hard reload.

The mirror case was equally broken and easier to overlook: a guest who recovered
into a host account got NO host affordances.

`clearAuth` already had a hook registry for exactly this shape of problem, with a
comment explaining it exists to avoid circular imports. Add the missing mirror,
`onSetAuth`, fired by both `setAuth` and `setAdminAuth` after the new token is
resident, and have the role store register on both sides: clear to null on
logout, re-seed from the new token on login. That also gives
`syncRoleFromToken` — dead code with zero callers since I introduced it — its
intended purpose.

Seeding from the claim fixes the reported bug, but the claim is frozen for the
token's 30-day life, so a promotion or demotion still wouldn't reach the feed.
`/feed` now calls the existing `refreshEventState()` on mount, which fetches
`/me/context` and applies both the authoritative role and the lock/release state
in one request. The feed is the one route gating a destructive action on the role,
so it should not be the only route running on a stale claim.

Tests: 04-host/role-identity-reset drives the real flows. The first asserts the
host DOES see the action before asserting the newcomer does not — a negative
assertion alone would pass against a build that shipped no moderation at all. The
second covers the mirror, promoting a guest server-side while their resident token
still claims `role: guest`, so a fix that only cleared the role would fail it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 20:35:40 +02:00
fabi
be6d56f278 feat(moderation): let a host remove a guest's photo or comment from the UI
`DELETE /host/upload/{id}` and `DELETE /host/comment/{id}` were complete on the
backend — transactional, SSE-broadcasting, audit-logged — and had zero frontend
callers. The feed context sheet offered "Löschen" only when
`target.user_id === myUserId`, so the only lever a host actually had against an
unwanted photo was banning the uploader.

That is both disproportionate and ineffective. A ban doesn't retract what was
already posted, and it makes things strictly worse for comments: the ban check
runs BEFORE the ownership check on the guest delete route, so banning an abusive
author leaves their comment on screen and permanently undeletable by them. With
no host affordance, nobody could remove it at all.

- feed: hosts/admins get "Beitrag entfernen" on other people's posts, routed to
  the host endpoint (the guest route 403s anything the caller doesn't own) with
  moderation-specific confirm copy. Own-post "Löschen" is unchanged.
- lightbox: same for comments, via /host/comment/{id}.
- Ban semantics are deliberately untouched (USER_JOURNEYS §10 — banned users keep
  read access and cannot write). The deadlock is broken by giving the host a way
  in, not by loosening the ban.

Live role (this had to come first). `getRole()` decodes the JWT claim, but the
token is never reissued — the backend slides the session row forward and treats
the DB row as authoritative. The claim is therefore frozen for the token's
lifetime: up to 30 days. A guest promoted at the party saw no Host-Dashboard and
no moderation actions until they signed out and back in, even though
`/me/context` had been returning their real role on every page load and 4 of its
6 call sites dropped the field on the floor.

Add `role-store.ts`: seeded from the claim so there's no flash of the wrong nav,
then corrected by every `/me/context` response. Point the ad-hoc `getRole()`
callers at it (account, upload, host, admin, and the new feed gate). The host and
admin dashboards now derive `myRole` reactively, so a demotion disables their
controls immediately instead of at next login.

Tests: 04-host/moderation-ui drives the real UI — host removes a guest photo and
it's gone from /feed server-side; a plain guest is offered nothing on someone
else's post (the mirror that keeps the first test honest); a promoted guest gains
the dashboard on reload while their token still carries `role: guest`; and a host
removes the comment of an already-banned guest, asserting first that the author's
own delete 403s so the deadlock is real.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 22:33:33 +02:00
MechaCat02
3654aca18b feat(export): single-file offline keepsake viewer + guest download
Rebuild the guest "keepsake" (offline HTML gallery) so it renders when the
extracted index.html is opened over file://. Browsers block external ES-module
scripts and fetch() at origin null, so a normal multi-file SvelteKit build shows
a blank window. Build the viewer as one self-contained index.html (inlined
JS/CSS via vite-plugin-singlefile, standalone non-SvelteKit entry) and inject
the data as window.__EXPORT_DATA__; the backend embeds the built viewer via
include_dir! and writes the data global into index.html when zipping.

Also surface the guest-facing download: a feed banner and the BottomNav Export
tab, both shown once the host releases the export.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 17:28:54 +02:00
fabi
f8cba95e49 chore(frontend): add ESLint + Prettier; fix real findings; format the tree
The frontend had no JS/TS linter — only svelte-check. Add flat-config ESLint (typescript-eslint
+ eslint-plugin-svelte) and Prettier (tabs/single-quote, matching the existing style).

Rules encode "catch bugs, not enforce taste":
  - svelte/require-each-key KEPT — it is the exact bug class as the feed mis-tap fix. Fixed every
    flagged block: keyed activeFilters, filteredUsers, stagedFiles (by previewUrl), captionTags,
    admin tabs/jobs/users, the export-viewer suggestions/filters/comments, and the static skeleton
    loops.
  - svelte/prefer-svelte-reactivity KEPT — inline-disabled only the verified-safe sites (a local
    freq Map in a $derived.by, throwaway URLSearchParams query builders), with a reason each.
  - svelte/no-navigation-without-resolve OFF — wants resolve() around every goto()/href; taste, not
    a bug, and pure churn.
  - svelte/no-unused-svelte-ignore OFF — those comments are consumed by svelte-check, which ESLint
    can't see, so it wrongly calls them unused; removing them would reintroduce a11y warnings.
  - no-explicit-any OFF for *.test.ts only (partial fixtures legitimately use any).

Real code fixes beyond keys: removed a dead jobLabel(), an unused ViewerComment import and unused
catch binding, an unused scroll-lock arg, and replaced an empty interface with a type alias.

Then `prettier --write` (54 files). Formatting only. Verified: eslint clean, svelte-check 0 errors,
vitest 46 passed, vite build succeeds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 20:45:42 +02:00
fabi
c229b560d8 fix(feed,host): two mis-tap bugs where a live re-render destroys the click target
Both are the same class as the autocomplete bug fixed in bf68bc0: a handler must never
destroy the DOM node that is being clicked.

feed grid tiles: VirtualFeed keys tiles by upload.id but slices the uploads array
POSITIONALLY (`uploads.slice(i*COLS, …)`). A `new-upload` SSE prepends to the array, so
every tile shifts one slot and each row's keyed {#each} sees a new set of ids — Svelte
destroys and recreates the tile nodes. At a party, where photos stream in continuously, a
guest mid-tap can have the node torn out (tap swallowed) or, worse, like/open a DIFFERENT
photo that slid under their finger. Grid now buffers live arrivals behind the existing
"neue Beiträge" pill; list view is keyed at the top level and stays live.

host rebuild button: it lived inside an SSE-driven {#if exportGenerating}{:else if
exportReady}{:else} block, and rebuildExport() makes the backend broadcast export-progress
immediately — so pressing it flipped the branch and unmounted the button mid-click, on the
one screen whose purpose is recovering a broken keepsake. One button now stays mounted
across all three states; only its label and disabled vary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 07:25:19 +02:00
fabi
bf68bc08f4 fix(feed): commit filter suggestions on click, not mousedown
The `filter-search` spec was flaky (~3%: 5 failures in 180 runs), always as
"element was detached from the DOM" while clicking a suggestion. It was not a test
bug — it was reporting a real defect.

`selectSuggestion` sets `showAutocomplete = false`, which unmounts the dropdown, so a
suggestion button DESTROYS ITSELF when activated. It was wired to `onmousedown` — the
first event of the click sequence — purely to beat the input's `onblur`, which would
otherwise close the dropdown before a click could land. So whether the node survived to
`mouseup` depended on whether Svelte's flush happened to fall between the two events.
Under load it did, and the click was lost. That is also a real user-facing bug: because
it committed on PRESS, sliding off a suggestion you didn't mean to hit still applied the
filter, with no way to abort.

Fixed the standard combobox way: preventDefault on the dropdown's mousedown suppresses
the blur, which is the only thing onmousedown was buying — so the handler moves to
`onclick`, the LAST event, where self-destruction is harmless and press-then-slide-off
aborts like a button should. Because the input now keeps focus across a selection,
`onfocus` no longer re-fires, so reopening is also wired to click/input — without that
the picker could not be reopened without clicking away first.

Also freezes the suggestion SOURCE while the dropdown is open. `allTags` is ordered by
frequency and derives from `uploads`, which every `upload-processed` SSE replaces via
refreshFeedInPlace — so an arriving photo could REORDER the open dropdown under the
user's finger. At a party, where photos stream in continuously, that is a live mis-tap
hazard. This was NOT the cause of the flake (I first believed it was; a clean 60-run
said so and a 120-run refuted it), but it is a genuine defect on the same interaction,
so it stays.

Verified: 240/240 consecutive passes on the previously-flaky spec (baseline 2.8%, so
~0.1% odds of luck), full suite 162 passed / 1 skipped with zero failures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 22:06:56 +02:00
fabi
36fe59caa5 fix(user-flow): persona-audit fixes — ban replay, locked-upload data loss, host UX, admin session
Follows the perf + security + user-flow work with a role/persona audit (guest, host,
admin, projector) and fixes across three review rounds. Highlights:

HIGH
- Ban now replays on reconnect. A ban isn't a soft-delete, and the `user-hidden` SSE has
  no replay, so a client that missed it (esp. the unattended diashow) kept cycling a
  banned user's slides. New `uploads_hidden_at` (migration 013) + `hidden_user_ids` in
  /feed/delta; feed + diashow evict those users. Applied even on a truncated delta.

MEDIUM
- Locked-upload data loss: a photo staged offline during a lock/release was purged as a
  terminal 4xx and lost when the host reopened. New reversible `uploads_locked` error code;
  the queue keeps the blob and auto-resumes on the `event-opened` SSE.
- Reopen after release now warns (ConfirmSheet) that it revokes the published keepsake.
- Host "forgotten-PIN" badge updates live (`pin-reset-requested` was broadcast but never
  in KNOWN_EVENTS / subscribed); host page also refetches on `pin-reset` so a two-host
  race can't hand out a conflicting PIN.
- Ban modal copy fixed (read-only ban, not "session ended"); Degradieren/Sperren/Entsperren
  hidden on peer-host rows for non-admins (they always 403'd).
- Host dashboard shows live keepsake generation progress / ready state + link to /export.
- Admin JWT moved to sessionStorage (§11.1) to bound exposure on shared devices.

Export generation guard (H1 from the prior round, hardened): per-(event,type) `release_seq`
(migration 012) with seq-guarded claim/finalize/mark_failed/update_progress, per-generation
temp/final paths, download follows `file_path`, prune only strictly-older generations.

LOW: diashow coalesces upload-processed (avoids self-rate-limit); event-closed reconciles
galleryReleased; /recover gains a forgot-PIN request + drops a stale cached PIN on 401;
delta `>=` tie-break + 429 retry; misc copy/labels.

Adds e2e: ban-replay, upload-lock-code, and rewrites export-reopen-rerelease with a
data-completeness test. Reconciles USER_JOURNEYS §9/§11.

Verified: cargo build clean, 40 unit tests, svelte-check 0 errors, 33 frontend unit
tests, 155 e2e passing on chromium-desktop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 21:06:28 +02:00
MechaCat02
641174717c fix(user-flow): close offline-queue, export, session, ban & realtime gaps
Some checks failed
E2E / Playwright E2E (chromium-desktop) (push) Failing after 6m52s
E2E / Cross-UA smoke matrix (push) Failing after 3m50s
Comprehensive user-flow review across guest/host/admin roles, then fixes with
e2e regression guards. Highlights:

- Offline upload queue auto-resumes on reconnect: network errors keep items
  pending (not error), 4xx are terminal (no infinite retry), quota exhaustion
  returns a distinct 413; queue cap + dedup.
- Export lifecycle: release atomically locks uploads; reopen invalidates and
  re-release regenerates the keepsake; workers claim their job atomically so a
  reopen->re-release can't corrupt the ZIP; startup re-spawns interrupted
  exports.
- Sessions slide on activity (no 30-day cliff); JWT expiry deferred to the
  revocable session row; sign-out-everywhere + revoke-on-PIN-reset.
- Ban always hides content (v_feed / find_visible_media / export filter
  is_banned) but stays a read-only ban per USER_JOURNEYS §10 — sessions are
  not revoked, read access + keepsake download preserved.
- Realtime: server-clock SSE delta cursor; event-closed/opened drive the UI
  live; feed_delta rate-limited; like returns {liked, like_count} to fix
  multi-device drift; lightbox live comments; diashow delta backfill.
- Forgotten-PIN in-app request flow; simultaneous same-name join returns 409;
  quota increment is transactional; operator floor.

Adds e2e/specs/10-flow-review/ (offline resume, export integrity, deterministic
anti-race guard) and updates existing specs for the new contracts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:05:37 +02:00
fabi
d6c91974eb perf(backend): close config/upload/export hot paths + stability fixes
Performance:
- Cache the runtime `config` table in-memory (ConfigCache) with synchronous
  invalidation on every write (admin PATCH + test reseed). Was re-reading each
  key from Postgres on every request (~8 round-trips per upload).
- Stream uploads chunk-by-chunk to a temp file instead of buffering the whole
  body in RAM (peak was up to the per-class cap, e.g. 500 MB/video); only 512
  sniff-bytes are kept for magic-byte detection, then atomic rename into place.
- Cache the media-filesystem disk snapshot (DiskCache, 15s TTL) shared by the
  quota check and admin stats; drop the discarded System::refresh_all().
- HTML export streams video (and small-image) originals straight into the ZIP
  via a manifest instead of copying them to a temp dir first (removed the
  transient 2x disk usage) and drops the double directory scan.
- Auth extractor resolves session -> live user in one JOIN (was two queries),
  touching last_seen_at by token hash.

Stability:
- SSE: on broadcast lag, emit a `resync` event so the client runs a delta
  fetch instead of silently losing events; frontend reconciles adds, deletions,
  and (via an in-place refresh) like/comment counts on visible cards.
- Storage quota fails OPEN when the disk can't be read (was a 0-byte limit that
  locked out all uploads).
- Graceful shutdown drains in-flight requests on SIGTERM/SIGINT, bounded by a
  10s backstop so open SSE streams can't stall a deploy.
- Upload removes the persisted file if the DB transaction fails (no orphaned
  bytes with no row to reclaim them).

Tests:
- New pure select_disk() with 5 unit tests (longest-prefix, fallbacks, fail-open).
- New e2e export-video spec covering the HTML export's video-streaming branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 20:26:45 +02:00
fabi
0ed97f45cf fix(review-2): address re-review — close two blocker regressions + harden tests
The round-2 re-review (full suite green) found two real defects the passing
tests missed, both now fixed and covered:

B1 — banned users could still edit/delete their OWN content: edit_upload,
     delete_upload, delete_comment skipped the is_banned guard that upload/
     like/comment got (round-2 removed the global extractor block and missed
     these three). Added the guard (using auth.is_banned — no extra query).
     The moderation ban test now asserts 403 on DELETE upload, PATCH upload,
     and DELETE comment for a banned owner (+ upload survives) — it previously
     only exercised POST /upload, which is why the gap shipped green.

B2 — H3 live-eviction was dead code: 'user-hidden' and 'comment-deleted' were
     missing from KNOWN_EVENTS, so EventSource never listened and the handlers
     never fired. Added both. New browser-driven sse-eviction test asserts a
     hidden user's card actually evicts from an open feed with no reload — the
     backend-only SseListener checks couldn't catch this.

Minor items folded in:
- admin dashboard bounces a mid-session-demoted admin to /feed via live
  /me/context (mirrors the host page) instead of a dead error screen.
- feed "new posts" pill cleared on any full refresh (pull-to-refresh no longer
  strands it).
- corrected the stale sse.rs comment (banned users may hold a read-only stream).

Verified: backend 35 unit tests; svelte-check 0 errors; e2e 04-host + comment-ui
+ export-leak 13/13 on chromium (incl. the two new regression tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:45:57 +02:00
fabi
80179357a7 fix(review-2): high — read-only ban model, failed-compression cleanup, live SSE eviction
H1: corrected a round-1 over-reach. Bans are read-only per USER_JOURNEYS §10:
    the base AuthUser extractor no longer rejects banned users (they keep read
    access); Require{Host,Admin} and write handlers enforce the ban via
    auth.is_banned → 403 (incl. self-unban). Demoted hosts still lose powers
    immediately (live DB role) and are bounced off the dashboard. Also fixed the
    login/recover redirect regression on unauthenticated 401.

H2: failed compression now self-heals instead of leaving a permanent broken
    card — refund the quota, delete the orphaned original, soft-delete the row;
    the frontend toasts the uploader and evicts the card on upload-error.

H3: self-delete, ban-with-hide (user-hidden), and comment-deletes now broadcast
    SSE; feed, diashow, and lightbox evict the affected content live instead of
    waiting for a reload. sse-eviction.spec.ts covers it.

Riders: feed/+page.svelte also carries the medium truncated-delta pill; host.rs
also carries the low atomic release_gallery claim; admin/+page.svelte also drops
the dead compression_concurrency control (medium).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:19:43 +02:00
fabi
a084ba86c5 fix(review): CSP nonce for FOUC script + feed_delta truncation signal
Two items surfaced by the branch re-review:

CSP regression (blocking): script-src 'self' blocked the template-authored
anti-FOUC theme script in app.html — SvelteKit's mode:'auto' only hashes
scripts it injects. Added nonce="%sveltekit.nonce%" so it's substituted per
request and included in the CSP (survives future edits, unlike a pinned hash).
Verified: emitted CSP nonce matches the script tag; no violation, no flash.

feed_delta silent truncation: the LIMIT 200 (added earlier to kill the
stale-`since` DoS) returned only the newest slice with no signal, so a client
that missed 200+ uploads during a reconnect could not tell it should full-
refresh — the older missed uploads were dropped and unrecoverable (the next
delta advances `since` past them). DeltaResponse now carries `truncated`; the
feed's feed-delta handler calls loadFeed(true) to resync from page 1 instead
of merging a partial slice.

Verified: cargo check clean, svelte-check 0 errors, production build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:51:08 +02:00
fabi
74849c8d50 test: cover grid filter OR/AND (extract pure fn + unit + real e2e)
The two filter-search cases were test.fixme placeholders (expect(true).toBe(true)),
so the OR/AND chip rules were untested.

- Extract the grid-filter predicate from feed/+page.svelte into a pure
  filterUploads(uploads, filters) in $lib/feed-filter (behavior-preserving) and
  unit-test it (9 cases): tag OR, user OR, tag+user AND, AND-excludes-partial,
  case-insensitive caption match, null caption.
- Replace the e2e fixmes with real tests that seed known captions/uploaders, switch
  to grid view, activate chips via the search suggestions, and count grid tiles:
  OR widens 1→2, AND narrows 2→1.

Frontend unit: 27 passing. filter-search e2e: 3 passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 20:08:28 +02:00
fabi
e79e020566 feat(eventsnap): UX review batch-3 — feed virtualization, a11y/UX fixes, shared primitives
Frontend
- Feed: DOM-windowing via @tanstack/svelte-virtual (new VirtualFeed). The window
  virtualizer keeps the sticky header, pull-to-refresh, infinite-scroll sentinel and
  bottom nav working. List = dynamic measured heights keyed by upload id +
  anchorTo:start so an SSE prepend doesn't jump a scrolled reader; grid = measured
  square rows. Drops the content-visibility band-aid and the old FeedGrid.
  measureElement(null) on row unmount prevents ResizeObserver retention; stable
  option callbacks + guarded setOptions avoid O(n) re-measure on like/comment patches.
- Global: viewport-fit=cover (activates safe-area insets), lang=de, PWA manifest +
  maskable icon + apple-touch metas, prefers-reduced-motion, safe-area-top headers.
- Feed reactivity: like/comment SSE patch the single card in place; upload-processed
  debounced in-place merge; IntersectionObserver leak fixed; shared 60s clock.
- CLS: list cards reserve the skeleton aspect box.
- Destructive actions (promote/demote/unban, gallery release) routed through
  ConfirmSheet (host + admin).
- Export OOM: streamed download via single-use ticket instead of res.blob().
- Shared primitives: IconButton (44px hit area), scrollLock action; UploadSheet a11y.
- Polish: api timeout + non-JSON guard, focus-trap offsetParent fix, pull-to-refresh
  passive:false + guards, diashow keyboard a11y, Toaster assertive errors, dark-mode
  gaps, aria-pressed on like/chip state, CameraCapture mic-on-video + retry,
  ConfirmSheet spinner, upload beforeunload warning, emoji->SVG icons.

Backend
- social.rs: like/comment SSE broadcasts now carry the fresh count so feed clients
  patch one card in place instead of refetching page 1.
- admin.rs / main.rs: supporting changes for the above.

Verified: svelte-check 0 errors, frontend build clean, /feed SSR 200. Runtime feed
scroll-feel validation still owed (documented in FOLLOWUPS.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 19:16:48 +02:00
MechaCat02
309c25bc06 feat(frontend): UX review followups — primitives + a11y/UX fixes across 4 passes
New shared primitives:
- Toaster + toast-store, ConfirmSheet, Modal, focusTrap action,
  pullToRefresh action, avatarPalette + initials helper, Skeleton,
  HeartBurst, haptics, export-status store with onClearAuth hook

Critical UX/a11y:
- Replaced window.confirm with branded ConfirmSheet
- Focus management + Escape on every modal (PIN, Lightbox,
  Onboarding, ContextSheet, data-mode sheet, leave-confirm,
  HTML guide, host/admin ban + PIN-display modals)
- Sheet backdrops are real buttons with aria-label
- Silent ApiError catches now surface via global Toaster

Major polish:
- Dark-mode parity on HashtagChips + avatars (shared palette)
- Conditional Export tab in BottomNav (badge dot when ZIP ready)
- Back chevrons on /recover (history-aware) and /export
- Upload composer discard confirmation when content is staged
- Camera segmented Photo/Video shutter
- PIN auto-submit on 4th digit, paste-flash-free (controlled input)
- Welcome-back toast on /feed after PIN recovery

Minor:
- Skeleton states on feed; pull-to-refresh with live drag indicator
- Haptics on like / capture / submit / PIN-copy / onboarding complete
- Comment 500-char counter; quota "Fast voll" / "Limit erreicht" labels
- Onboarding pip ≥24px tap targets; long-press hint step
- overscroll-behavior lock on <html> while feed mounted
- teardownExportStatus wired via onClearAuth (covers 401 + explicit logout)
- ConfirmSheet per-instance titleId; Modal requires titleId or ariaLabel

Tests (7 new Playwright specs):
- 01-auth/pin-auto-submit, 01-auth/back-chevron
- 03-feed/confirm-sheet-delete, 03-feed/toast-on-failure
- 09-mobile/focus-trap, 09-mobile/sheet-escape,
  09-mobile/upload-cancel-confirm

FOLLOWUPS.md captures the deferred AT inert containment work
with acceptance criteria + implementation sketches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:50:28 +02:00
MechaCat02
e619a3bd64 feat(ui): v0.16 features + dark mode across every page
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>
2026-05-16 14:33:30 +02:00
MechaCat02
4a5506f32d feat: mobile-first UI redesign (v0.15.0)
- Persistent bottom tab bar (Feed · FAB · Account) on all authenticated pages
- Upload FAB triggers bottom sheet (Galerie / Kamera) → navigates to composer
- Upload page redesigned as full-screen composer with thumbnail strip, textarea,
  quick-tag chips, sticky submit button; bottom nav suppressed while composing
- Slim upload progress bar above bottom nav driven by queue state
- Feed: list/grid view toggle; list = chronological full-width FeedListCard;
  grid = 3-col with search bar, autocomplete from loaded posts, filter chips
- Account page: role-gated dashboard links (Host / Admin); Konto section with
  leave-confirm bottom sheet; no more per-page header nav icons
- Host dashboard: back arrow, collapsible sections, 2-col stats, user search
- Admin dashboard: back arrow, inner tab bar (Stats/Config/Export/Nutzer),
  stacked config inputs with sticky save, new Nutzer tab
- BottomNav hidden on unauthenticated pages via isAuthenticated store
- FeedGrid: threeCol prop; OnboardingGuide upload step updated for FAB
- Concept docs added to docs/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 18:40:57 +02:00
MechaCat02
4757be71a3 feat: role-aware nav icons across feed, host, and admin pages
Feed: shows star icon (→ /host) for host/admin, shield icon (→ /admin)
for admin only, alongside the existing account icon.
Host: shows shield icon (→ /admin) for admin only next to "Zur Galerie".
Admin: replaces "Host-Dashboard" text link with star icon (→ /host).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 20:08:37 +02:00
MechaCat02
87b5aff478 feat: implement onboarding guide and HTML export guide
Add 4-step dismissible onboarding overlay shown on first feed visit
(welcome, upload, hashtags, PIN importance). Dismissed state persisted
in localStorage under eventsnap_guide_seen. Step indicator dots and
skip/continue buttons included.

Update HTML export guide modal to persist the eventsnap_html_guide_seen
flag: first download shows the instructions modal; subsequent clicks go
straight to download without interruption.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 21:13:14 +02:00
MechaCat02
75d186fad3 feat: implement My Account page
Add /account route showing display name (from localStorage), role badge,
session expiry decoded from JWT, and recovery PIN display with copy button.
Join and recover flows now persist display_name to localStorage via setAuth().
Feed header logout button replaced with person-icon link to /account;
logout is available from the account page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 21:11:11 +02:00
fabi
964598e41d feat: implement gallery feed with social features and SSE
- 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>
2026-04-01 19:17:06 +02:00
fabi
8b9d916265 feat: implement authentication flow
Backend:
- AppConfig, AppError, AppState modules for shared infrastructure
- JWT creation/verification with HS256 (jsonwebtoken crate)
- Session management: SHA-256 token hashing, DB-backed sessions
- Auth middleware: AuthUser, RequireHost, RequireAdmin extractors
- POST /api/v1/join: name-only registration, 4-digit PIN + bcrypt hash
- POST /api/v1/recover: PIN-based recovery with 3-attempt lockout (15 min)
- POST /api/v1/admin/login: bcrypt password verification
- DELETE /api/v1/session: logout (session invalidation)
- Migration 006: user PIN lockout columns (failed_pin_attempts, pin_locked_until)
- Models: Event, User (with role enum), Session with all CRUD methods

Frontend:
- api.ts: typed fetch wrapper with automatic Bearer token injection
- auth.ts: JWT/PIN localStorage management with Svelte store
- /join: name entry form with PIN display modal and copy button
- /recover: name + PIN recovery form with saved PIN pre-fill
- /feed: placeholder gallery page with logout
- Root layout: auth initialization on mount
- Root page: redirect to /join or /feed based on auth state

All responses use German language strings as specified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 21:44:03 +02:00