0fba8defc2696b0671ba7e0c5902202f6316b5f4
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2dd563b3ee |
fix(upload): a truncated body no longer destroys the guest's photo, and the in-app camera can be switched off
Some checks failed
Audit / cargo audit (backend) (push) Failing after 9m31s
Audit / npm audit (frontend) (push) Successful in 1m2s
Checks / Backend — cargo test + clippy + fmt (push) Failing after 1m7s
Checks / Frontend — vitest + svelte-check (push) Failing after 5m37s
Checks / Keepsake viewer — builds, self-contained, committed artifact in sync (push) Failing after 5m13s
Checks / E2E — typecheck + lint (push) Failing after 36s
E2E / Playwright E2E (chromium + webkit) (push) Failing after 9m8s
E2E / Cross-UA smoke matrix (push) Failing after 4m17s
Two event-day failures, both frontend-only.
TRUNCATED UPLOADS PURGED THE PHOTO. An iPhone guest uploading from the gallery
inside WhatsApp's browser got "Error parsing `multipart/form-data` request" and
the item went to "Gesperrt" with no retry. That message is AXUM's own multipart
rejection — a plain-text 400, no JSON envelope — which means the request body
never arrived intact. It is a transport failure, not a verdict on the file.
`classifyUploadStatus` maps every 4xx to `terminal`, and terminal PURGES the blob
from IndexedDB and offers no retry. So a webview hiccup deleted the only copy the
guest had, and told them the photo was rejected.
Every 400 the app itself raises carries `bad_request` in a JSON envelope (too
large, wrong type, caption too long, NUL byte), so an unparseable 400 is
distinguishable and is now a NetworkError: blob kept, retry offered. This is the
same rule the 403 branch already applies — "an unparseable body must NOT purge
the blob, losing a photo is the worst outcome" — extended to the status that was
actually hit. Retrying is safe because nothing was parsed, so nothing was stored
and no quota was charged, and `X-Client-Upload-Id` makes a duplicate impossible.
IN-APP CAMERA SWITCH. `PUBLIC_CAMERA_ENABLED=false` removes the "Kamera — Jetzt
aufnehmen" entry from the upload sheet. On some phones `getUserMedia` fails when
switching front/back ("Kamera konnte nicht gestartet werden") or when asked for
video, and those failures are per-device and undiagnosable mid-event; the switch
removes the broken path rather than leaving guests to find it. Nothing is lost:
the gallery picker reaches the phone's own camera app and handles video.
Read at RUNTIME via `$env/dynamic/public`, so flipping it is a compose variable
and `up -d frontend`, not a rebuild. Deliberately NOT routed through the
backend's event payload like `comments_enabled`: that flag describes the event,
this one describes what the client can do — the backend cannot tell a camera
upload from a gallery upload and has no stake in it. Keeping it off the app image
also means no backend release on the day of the event.
The onboarding step and the in-app-browser hint drop their camera wording when it
is off, so no text promises a button that is not there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
5a33ab460f |
fix(copy): five texts that told guests the wrong thing
None of these are cosmetic — each one either misdescribes the UI or omits
something a guest needs to get back into their account.
JOIN. "Willkommen bei" + "<Event>" read as "Willkommen bei Hochzeit von …".
Adds the article. This deliberately couples the lead-in to EVENT_NAME's
grammatical gender and is wrong for "Willkommen bei der Sommerfest" — noted in
the markup, with the article-free alternative, for whoever reuses this. The
no-name fallback moves from "dem Event" to "Feier", or it would now render
"Willkommen bei der dem Event".
GUIDE / post actions. Named only the long-press, which is invisible: a guest has
to already know it exists to find it. Now names the three-dot button that is on
every card in list view — described as "die drei Punkte (⋯)" rather than a
glyph, because the icon renders HORIZONTALLY (three circles at cy=12) despite
the code comment calling it a kebab. Long-press stays in the text, because the
button exists ONLY in the list view: the grid tiles have `use:longpress` and no
button at all, so a menu-only instruction would strand anyone browsing in grid
mode. "anzeigen und speichern", not "herunterladen" — the action opens the
original inline in a new tab (Content-Disposition: inline, deliberate, it is the
only playable video source), so the guest saves it from there.
GUIDE / recovery. Said "Deinen PIN merken!", but `POST /recover` takes
{display_name, pin} — a guest who memorised four digits and forgot whether they
typed "Anna" or "Anna M." still cannot get in. Now names both. It also claimed
the PIN is "immer unter Mein Konto zu finden", which is false in the only case
that matters: /account renders it from local storage and falls back to "PIN
nicht gespeichert", so it is NOT on the new device you are trying to reach.
UPLOAD / photo-task game. Adds a persistent helper line under the caption box
rather than a second placeholder line: a `placeholder` attribute may not contain
line breaks (Safari collapses them), and a placeholder disappears exactly when
the guest starts typing and needs it. The hashtag is written plain on purpose —
`#fotoaufgabe3` is a DIFFERENT tag from `#fotoaufgabe`, so putting the number
inside it would turn twelve tasks into twelve unfilterable tags.
UPLOAD SHEET. "Öffne den Link in Safari oder Chrome" named a link that is not on
screen — the guest is already inside the app. Now names the situation (the
WhatsApp in-app browser the join link opens in) and the menu entry that escapes
it. Kept rather than removed: in that webview the camera and file picker can
silently do nothing, with no error and no operator to ask.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
2b1500e624 |
fix(ui): make the dashboards agree with each other and with what the code does
Host and admin implement the same four operations with independently written copy, and admin was stale or wrong in every case. Its release button was always enabled and always read "Galerie freigeben", so a second tap returned a 409; it showed no release state, no keepsake progress, no failure reason, no rebuild, and never refreshed after releasing. It now matches the host page. Both dashboards subscribed to SSE and never opened the connection — `onSseEvent` only registers a handler. Every subscription was inert, so the keepsake progress bar sat frozen after a release and PIN requests appeared only on a manual refresh. It happened to work when arriving straight from /feed, which connects, and /feed disconnects on destroy, so navigating to the dashboard killed it again. The unban confirm named neither of the two things a host most needs to know: unbanning also restores ALL of that guest's previously hidden photos to the gallery, diashow and export, and it retires and rebuilds a released keepsake, during which every guest's download is briefly unavailable. The ban modal warns that uploads vanish; nothing said they come back. Both now do, gated on the gallery actually being released. "Event verlassen" implied the account was being deleted, then the dialog said the guest could log back in. It calls `DELETE /session` — this device only, nothing deleted — so it is "Abmelden" now. Gallery release now states it locks uploads and is reversible; PIN reset states the guest is signed out on all devices. The keepsake download failed silently: nothing inspected the iframe result and the ticket POST always succeeded, so an over-limit tap did nothing at all. It now surfaces the (newly visible) 429 and confirms the download started. `/export` rendered "Export noch nicht verfügbar / Schau nach der Veranstaltung noch einmal vorbei" when the status request had merely FAILED — telling a guest to come back after an event that already happened. Both dashboards' error states gained a retry, which a host on a PWA with no URL bar otherwise has no way to reach. Modals were centred with no max-height, so on a short viewport the join PIN dialog clipped equally top and bottom — potentially putting "Weiter zur Galerie" off-screen at the moment a first-time guest must proceed. The ten moderation buttons were ~28px tall side by side, on the screen where a mis-tap bans the wrong guest; they are 44px now. Six German quotation marks paired the opening „ with an ASCII straight quote. Co-Authored-By: Claude Opus 5 <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>
|
||
|
|
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> |
||
|
|
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> |