a3d8ae72e3c6bcebe0f4284e312989b483213ee7
19 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
43c2a0d09c |
feat(host): warn about low disk before it becomes unrecoverable
Storage visibility existed in exactly one place: a passive Speicherauslastung widget on the ADMIN dashboard. A host who isn't the admin had no view of it, and nothing warned anyone. README carried "Low-disk alert (< 10 GB free)" under Planned since v1. Two things make this a safety net rather than a nice-to-have. postgres_data, media_data and exports_data are all Docker named volumes on ONE filesystem, so running out doesn't degrade a subsystem -- Postgres stops being able to write and the whole event goes down. And the keepsake needs room for two gallery-sized archives, which the export preflight can only ever refuse AFTER the release, when the event is over and every remedy is harder. So the threshold is not a fixed number alone. It fires on the 10 GB floor the README always named, OR on "you could not build the keepsake right now" -- the trigger a host can still act on, computed with the same arithmetic the preflight uses. Unknown free space is NOT low: it fails open like the upload quota and the preflight do, because a banner that cries wolf on an unreadable mount is a banner nobody reads. Carried on GET /host/event, which the dashboard already fetches on load and on every reload -- no new endpoint, no new poll. Rendered above everything else including the PIN-reset queue, and it names the consequence (the event, not just the download) rather than only the number. Also fixes the host page's formatBytes, which topped out at MB: 30 GB free would have rendered as "30720.0 MB", and a guest with 2 GB of uploads was already being shown that way in the user list. Tests: 5 unit on the threshold (including that plenty of free space is still low when the keepsake wouldn't fit -- the case a fixed threshold misses entirely), 3 e2e. The e2e drives it through `original_size_bytes` rather than a genuinely full disk: the estimate is pure SQL over that column, so overstating one row moves the accounting without touching a byte on disk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
281eb3bec7 |
fix(export): refuse an export that cannot fit, and stop peaking at two generations
Nothing in export.rs ever asked whether the keepsake would fit. Both archives write
their media `Compression::Stored`, so each is essentially a byte-for-byte second copy
of the originals -- Gallery.zip always, and Memories.zip for every video and every
image at or under 5 MB. On the documented CX33 (80 GB, all three volumes on one
filesystem) the upload quota's fixed point leaves ~40 GB free, and a release spawns
BOTH halves concurrently against it.
The failure is not "the export failed", it is "the deliverable is stuck":
1. ENOSPC lands partway through a multi-GB write.
2. The epoch has already moved, so the job row is `failed` at the CURRENT
generation and readiness (epoch = event.export_epoch AND status = 'done') is
false -- GET /export/zip 404s.
3. The last good archive sits on disk, unreferenced and unreachable.
4. POST /host/export/rebuild, the only escape, re-arms the same doomed write.
Three changes.
Reclaim before building. `prune_stale_export_files` ran only after the new archive
was written, renamed and finalised. That reads as durability but buys nothing: the
moment `invalidate_and_arm` bumps the epoch the old archive is ALREADY unreachable,
so keeping it reserves gigabytes for a download nobody can perform -- and for a
takedown it is content someone explicitly asked to have removed. Peak usage is now
one generation. Narrower than the post-finalize prune on purpose: final archives
only, never a `.tmp` or a `viewer_tmp_` dir, since a superseded worker can still be
streaming into those and at build START is far more likely to be alive.
Preflight the space. SUM(original_size_bytes) over exactly `query_uploads`'
visibility filter, +10% for ZIP overhead, multiplied by the number of armed jobs --
without that multiplier each of the two concurrent halves independently sees "it
fits" and together they don't. Runs AFTER claim_job, not before as reported: bailing
before the claim leaves the row `pending` with no worker and no error, the
spinner-forever state `mark_failed`'s status guard exists to prevent. Fails open when
the mount can't be read, exactly as the upload quota does.
Show the host the reason. /export/status returned {status, progress_pct} and nothing
else, so the host dashboard could only render "fehlgeschlagen" next to the retry
button. The message was written to the row and surfaced solely in the ADMIN job list
-- a different screen, possibly a different person. It now travels with the status,
and only on a failure, so a message left on a since-succeeded row can't appear beside
a green "ist bereit".
Tests: 10 unit (the u128 clamp caught a real bug in the first draft -- saturating_mul
then /100 turns an overflow into a number ~100x too small, the one direction that
authorises the write being guarded against; the carried-forward archive must survive
its own older epoch in the filename), 4 DB-backed (the estimate is asserted against
the row set the archive actually contains, not against a restatement of the WHERE
clause, so the two queries cannot drift), 3 e2e over the four-hop plumbing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
3fb1b5d80d |
feat(comments): hide every comment mention when COMMENTS_ENABLED=false
The kill-switch previously left comment UI/text visible in several places. Sweep the whole surface so a comments-off instance shows no trace: Frontend (main app): - VirtualFeed grid tile: gate the comment button/count on $commentsEnabled (was ungated — the only feed surface still showing it). - admin stats: hide the "Kommentare" count card. - UploadSheet "Uploads geschlossen" notice, host ban-modal description, and host/admin unban confirmations: drop the "…und kommentieren" wording. - export page: drop "Kommentaren" from the keepsake description. Keepsake export (had no concept of the flag): - export.rs: thread comments_enabled into the exported data (ViewerEvent), wired through spawn_export_jobs/recover_exports and their call sites (host.rs, main.rs). - export-viewer: gate comment counts (list + grid) and the lightbox comments section; older exports without the field default to enabled (?? true). Backend still 403s comment writes when disabled (unchanged) — this is the UI half so stale clients and archives match. Verified on the running stack (COMMENTS_ENABLED=false): /event reports comments_enabled=false, a regenerated keepsake embeds "comments_enabled": false with no comment UI, and the uploads-closed notice renders "…ansehen und liken." Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f243bfe89a |
feat(ui): elegant wedding white/silver/gold redesign
Reskin the whole app via design tokens in tailwind-theme.css (remapped color ramps) plus a define-once component layer in lib/styles/components.css (.btn, .card, .input, .chip, .badge, .sheet, …). Buttons are muted/outlined gold rather than flat fills. Self-host Inter + Fraunces (woff2) under the existing font-src 'self' CSP. Restyle the shared components and the account, admin, host, join, recover and upload screens against the new tokens. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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
|
||
|
|
3c683247c0 |
chore(deploy): expose the export rebuild hatch in the UI; rehearse migration 014
Three deploy-readiness gaps, none of them in the export invariant itself. 1. The escape hatch was unreachable. `POST /host/export/rebuild` was added as the fix for "a failed export is terminal", but nothing in the frontend called it — so recovery required a terminal, the API docs and a valid JWT. The host page instead told the host to reopen uploads and re-release, which is the destructive act the endpoint exists to avoid (it unlocks the gallery to every guest and retracts the release). The failed state now offers "Erneut versuchen"; a ready keepsake can be rebuilt behind a confirm, since a rebuild makes it briefly undownloadable. 2. Migration 014 had never been run against anything. It is the repo's first destructive migration and its backfill has to carry the old notion of "downloadable" across exactly — get it wrong and a released event's keepsake silently 404s, on data that cannot be re-created. backend/scripts/rehearse-014.sh proves it on a throwaway postgres, against a real pg_dump or a synthetic DB covering every pre-014 state, asserting up, down and up-again all preserve downloadability. Verified non-vacuous: retiring nothing (ELSE -1 -> ELSE e.export_epoch) fails it, naming the three keepsakes it would resurrect. It also surfaced that the down migration is an inverse UP TO DRIFT: a ready flag that disagreed with its job row comes back FALSE. That heals corruption rather than restoring it, and costs nothing (no file_path to serve), so the assertion checks what actually matters — no genuinely downloadable keepsake loses its flag — and reports the normalisation instead of failing on it. 3. No advisory scanning. cargo audit + npm audit, in .github/workflows/ because Gitea Actions scans it too and e2e.yml already resolves actions/* from GitHub. The runner is not assumed to have cargo. RUSTSEC-2023-0071 (rsa/Marvin) is ignored SPECIFICALLY, not globally: it is unreachable here (HS256 + bcrypt, Postgres only) and unpatched, so failing on it would mean a permanently red pipeline everyone learns to ignore. Tests: e2e pins that a failed keepsake recovers via rebuild while the event stays released and uploads stay locked — so a future refactor implementing rebuild as an internal reopen+re-release fails — and that rebuild cannot publish an unreleased gallery. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
811c724685 |
fix(audit-followup): close regressions/gaps from the persona-audit re-review
Adversarial re-review of the persona-audit commit surfaced two HIGH regressions I'd introduced plus MED/LOW gaps. All fixed: HIGH - Auth privilege escalation: reads are sessionStorage-first, but guest `setAuth` wrote only localStorage — a resident admin token shadowed a new guest login (guest ran as admin on a shared device). setAuth/setAdminAuth now DISPLACE the other store so exactly one identity is resident. Adds unit + e2e regression guards. - Host dashboard: `exportGenerating` used `||`, so a one-half export failure stuck on "wird erstellt…" forever and never showed the re-release hint. Changed to `&&`. MEDIUM - Locked-upload auto-resume was unreliable (`event-opened` has no reconnect replay and SSE is down on non-feed pages) — now also resumes on `feed-delta`, which fires on every SSE reconnect. - Queue-cap eviction could drop a recoverable locked item (parked as `error` with a live blob) — now evicts only `done`/`blocked`. - Host page async-`onMount` leaked SSE handlers if unmounted mid-load — added a `destroyed` guard before registration. LOW - An unparseable 403 body now keeps the blob (reversible) instead of purging it. - `refreshEventState` is sequence-guarded so a late close-refresh can't clobber a reopen. - `/export/status` moved out of the all-or-nothing `reload()` so its failure can't blank the whole host dashboard. Verified: svelte-check 0 errors, 36 frontend unit tests (incl. new displacement guards). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
641174717c |
fix(user-flow): close offline-queue, export, session, ban & realtime gaps
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>
|
||
|
|
faf7a2504a |
fix(audit): restore broken upload pipeline + role-based E2E audit fixes
A comprehensive role-based E2E audit (guest/host/admin, across browser sessions) surfaced one critical and several smaller issues; this addresses them and hardens the tests that missed them. Critical - The client upload pipeline was fully broken: the IndexedDB v1->v2 upgrade opened a *new* transaction inside the upgrade callback, which throws during a version-change transaction and aborted the whole upgrade, leaving the queue object store uncreated -- so no UI upload ever fired. Reuse the version-change transaction the callback provides, and bump the DB to v3 with a contains() guard so installs already corrupted by the shipped bug self-heal on next load. Re-enabled the previously-fixme'd UI upload E2E test. High / Medium - Event lock is uploads-only again: likes, comments and browsing stay open while the event is locked (USER_JOURNEYS 9.3 / FEATURES) -- it was wrongly freezing social interaction. Updated the event-lock spec accordingly. - get_original now excludes soft-deleted and ban-hidden uploads, and direct /media/originals/** serving is blocked, so a hidden user's originals can no longer be pulled by UUID (all originals go through the checked alias). - The upload handler reads the file field with an early-abort size cap chosen from the declared content-type, instead of buffering the entire body before the size check. Low - unban_user mirrors the ban role guard (a host can no longer unban a host/admin banned by an admin). - reset_user_pin's UPDATE is event-scoped. - Admin login returns and stores a real identity (user_id + display name) instead of a blank session. - The host user list no longer renders target-actions (ban/promote/demote/PIN) on the caller's own row, where the backend always rejected them. - /diashow gains a client-side auth guard like the other protected routes. - The join page shows the event name via a new public GET /api/v1/event. Verified: backend cargo build clean, frontend svelte-check 0 errors, full Playwright E2E suite 144 passed / 1 skipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
71a2987a3e |
feat: implement host dashboard
Add Host Dashboard for event and guest management, accessible at /host.
Backend — new /api/v1/host/* endpoints (RequireHost auth):
- GET /host/event → event name + lock/release state
- POST /host/event/close|open → lock or unlock uploads; SSE broadcast
- POST /host/gallery/release → set release timestamp, enqueue export jobs
- GET /host/users → all guests with upload count & bytes
- POST /host/users/{id}/ban → ban with optional upload-hide choice
- POST /host/users/{id}/unban → lift ban
- PATCH /host/users/{id}/role → promote guest→host or demote host→guest
- DELETE /host/upload/{id} → host-level soft-delete + SSE
- DELETE /host/comment/{id} → host-level soft-delete
Frontend — /host page:
- Event controls: lock/unlock toggle and release-gallery button with status badges
- Guest table: display name, role badge, upload count, storage used
- Ban flow: modal asking whether to keep or hide the user's uploads
- Promote/demote buttons respecting caller role (host can promote guests; admin can demote hosts)
- auth.ts: getRole() decodes JWT payload client-side to gate the route
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|