ef6d3a077ab8770910246e8c3999d9ea57512109
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
61119be817 |
Merge branch 'fix/deploy-unattended-blockers' into main
Two independent lines of production hardening diverged at |
||
|
|
51e55b1ace |
fix(feed): survive a bad network, and let the lightbox actually browse
Four things a guest on congested venue wifi would have hit, and one they would have
hit immediately.
A FAILED FEED LOAD CLAIMED THE GALLERY WAS EMPTY. `loadFeed` caught, toasted for five
seconds and left `uploads` empty, so the page fell through to "Noch keine Fotos. Tippe
auf den Kamera-Button unten!" — the most likely first impression at the party, and a
lie. There is now a distinct error state with "Erneut laden". Refreshes suppressed the
toast entirely, so pull-to-refresh and the "Neue Beiträge" pill failed in total
silence; they now report, and the pill survives its own failure instead of clearing
before the request.
THE FILTER-EMPTY STATE WAS DEAD CODE. With filtering server-side `displayUploads` is a
plain alias of `uploads`, so the grid's "Keine Treffer für die gewählten Filter." plus
its reset button sat behind an identical earlier branch and could never render — a
guest tapping a chip with no matches was told to go take a photo.
SSE COULD FREEZE THE FEED FOR THE WHOLE EVENING. Nothing in the feed ever refetched on
a timer; every update path was triggered exclusively by a stream event. Behind a proxy
that buffers `text/event-stream` `onopen` never fires, so the guest saw only the photos
that were on screen when they arrived; and a socket left half-open by an AP roam is
worse, because `connectSse` early-returns on a non-null EventSource and nothing ever
reconnects. A pure silence timer is not implementable — the backend sends keep-alives
as SSE comments, which the EventSource parser discards without dispatching — so
liveness is established on evidence instead: a jittered 60-120s `/feed/delta` backstop
that reconnects when a poll returns content the stream never delivered. The ticket
round-trip also seeds the delta cursor before the EventSource is created, so the
backstop has a `since` even if `onopen` never fires.
THE PILL COLLAPSED A DEEPLY-SCROLLED FEED to 20 items and dumped the guest at an
arbitrary scroll position — the exact yank the pill exists to avoid. It merges now.
The refresh debounce was 800ms + jitter, which during a burst is roughly one feed query
per client every two seconds; at 100 guests that approaches the 60/min per-user limit,
and the resulting 429s were swallowed by a bare `catch {}`, so the feed would simply
stop updating with no signal. Now 8s + jitter, coalescing, and skipped entirely while
the page is hidden.
Not one `<img>` in the app had an `onerror`. `pickMediaUrl` falls back to the original
whenever preview and thumbnail are null — i.e. for everything still compressing, which
during a burst is the top of the feed — so a 404 there rendered an empty grey box with
`alt=""`, not even a message. Each now retries once, then shows the placeholder.
The lightbox had no swipe, no prev/next and no arrow keys, so browsing 300 photos meant
closing and reopening the modal for every one — while FEATURES.md and USER_JOURNEYS
both claimed swipe shipped. It now has chevrons (44px, German aria-labels, hidden at
the ends), arrow keys, and horizontal swipe, with focus handed to the surviving control
so a disappearing chevron can't drop focus to `<body>`. Comment deletion was a ~14px
`✕` four pixels from the text that deleted permanently on one tap, while deleting a
POST two components away goes through a ConfirmSheet; it now matches.
`feed-filter.ts` and its test are deleted — with the server filtering, they were dead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
6fd75adb27 |
fix(ops): bound logs, drain ffmpeg's stderr, and stop three silent hangs
Six independent operational defects, none of which needed a new feature to fix. Log rotation. Docker's json-file driver is unbounded by default, and those files land on the HOST filesystem — outside every deploy.resources.limits in the compose file, and on the same disk as postgres_data and media_data. A full disk stops Postgres writing WAL, which takes the event down. Capped at 10m x 3 per service. Log level. RUST_LOG was set in neither .env.example nor docker-compose.yml, so the code fallback WAS the production level — and it was `debug`, with tower_http=debug emitting a line per request and per response into that unrotated file. Now info, with tower_http=warn to state that those spans are diagnostics, not an access log. ffmpeg pipe deadlock. run_ffmpeg piped stdout and stderr and then called wait(), which drains neither. Once the ~64 KiB pipe buffer filled, ffmpeg blocked writing and wait() never returned — burning the full 120s timeout, twice per seek position, three times per compression attempt. And the timeout is an Err, so the end state was a soft-deleted upload: a guest's playable video destroyed by a poster-frame failure. Now stdout is null (nothing ever read it) and stderr is drained by wait_with_output, whose tail is logged on a non-zero exit. Note wait_with_output consumes the child, so the old kill-on-timeout is gone; kill_on_drop(true) already covers it. Readiness probe. /health never touched the pool, so the disk-full endgame above stayed green all the way down. Adds /health/ready (SELECT 1 under 2s) as a SECOND route — the compose healthcheck deliberately keeps pointing at /health, because caddy gates its startup on it and a DB-dependent probe would turn a Postgres blip into the reverse proxy refusing to start. api.ts request timeout. The abort timer was cleared in a finally around fetch(), which resolves on the response HEAD — leaving res.text() uncovered and no longer abortable. An upstream that sends headers then stalls the body hung the call forever. The timer now lives until the body is read, including the 204 path (which otherwise leaked a live 20s timer per no-content request). Upload XHR watchdog. The XHR had no timeout while processQueue held the isProcessing latch across it; on a half-open socket neither error nor abort ever fires, so the latch pinned and the queue wedged. Bounds SILENCE rather than total duration — a 500 MB video over a venue uplink legitimately runs 30+ minutes while making steady progress. Rejects as NetworkError, which is already the retryable branch, so a stalled upload now recovers like any network blip. IndexedDB failures. addToQueue called getDb() unguarded and handleSubmit had no catch, so a private-mode refusal or a QuotaExceededError on a large blob left a permanent "Wird hochgeladen…" spinner, no toast, and — for an in-app camera capture — the only copy of the photo gone. Now reported as 'failed', which keeps the staged files on screen and stays on the page. Co-Authored-By: Claude Opus 5 (1M context) <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>
|
||
|
|
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> |
||
|
|
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> |