Commit Graph

10 Commits

Author SHA1 Message Date
MechaCat02
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>
2026-08-22 09:30:59 +02:00
MechaCat02
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>
2026-08-21 22:42:13 +02:00
Fabian Hamm (Privat)
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>
2026-08-03 18:36:57 +02:00
MechaCat02
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>
2026-07-18 17:28:42 +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
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
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