fix: post-review punch-list — upload concurrency cap, atomic release, ban SSE
Follow-up to the adversarial re-review of the audit-fix branch.
MUST-FIX:
- H3: bound aggregate upload RAM. The body limit alone didn't cap concurrency,
so ~N parallel 550MB uploads could OOM the box. Add an Arc<Semaphore>
(UPLOAD_MAX_CONCURRENCY=4) in AppState, acquired at the top of the upload
handler before the multipart body is read, so waiting requests hold only a
connection — peak buffered RAM ≈ 4×550MB. (Matches the CompressionWorker
semaphore pattern; avoids tower's non-default `limit` feature.)
- M8: release_gallery now runs the claim UPDATE + both job INSERTs in one
transaction, so the row lock is held until the jobs exist — closing the
cross-table TOCTOU where two concurrent presses could both spawn workers
racing the same output files. Export temp filenames are now per-run
(Gallery.{uuid}.zip.tmp, viewer_tmp_{event}_{uuid}, Memories.{uuid}.zip.tmp)
so overlapping runs can't truncate each other. Final served names unchanged.
- M11: 'user-banned' was missing from sse.ts KNOWN_EVENTS, so EventSource
silently dropped the frame and the forced-logout handler never fired. Added.
SHOULD-FIX:
- M8-3: re-release is now allowed when nothing is in progress and at least one
job failed (was: only when *every* job failed), so a one-sided failure (zip
done, html failed) is no longer permanently unrecoverable.
- M18: two light-mode contrast spots the earlier sweep missed — the LightboxModal
char-counter class: directives and the account PIN-missing notice.
- M15: the diashow auto-advance is now slowed to a ≥30s floor under
prefers-reduced-motion (WCAG 2.2.2), making the app.css comment accurate.
Verified: cargo build + 6 tests, npm run check (0 errors), and a live smoke test
against Postgres — first release 204, second 400, one-sided-failure re-release
204, no SQL errors; a normal upload still returns 201 through the new permit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -218,8 +218,8 @@
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 text-right text-xs"
|
||||
class:text-gray-400={newComment.length < 450}
|
||||
class:dark:text-gray-500={newComment.length < 450}
|
||||
class:text-gray-500={newComment.length < 450}
|
||||
class:dark:text-gray-400={newComment.length < 450}
|
||||
class:text-amber-600={newComment.length >= 450 && newComment.length < COMMENT_MAX}
|
||||
class:dark:text-amber-400={newComment.length >= 450 && newComment.length < COMMENT_MAX}
|
||||
class:text-red-600={newComment.length >= COMMENT_MAX}
|
||||
|
||||
@@ -38,6 +38,7 @@ const KNOWN_EVENTS = [
|
||||
'upload-deleted',
|
||||
'like-update',
|
||||
'new-comment',
|
||||
'user-banned',
|
||||
'event-closed',
|
||||
'event-opened',
|
||||
'event-updated',
|
||||
|
||||
@@ -219,7 +219,7 @@
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded-lg bg-white px-4 py-3 text-sm text-gray-400 shadow-sm dark:bg-gray-900 dark:text-gray-500">
|
||||
<div class="rounded-lg bg-white px-4 py-3 text-sm text-gray-500 shadow-sm dark:bg-gray-900 dark:text-gray-400">
|
||||
PIN nicht gespeichert. Nutze die Wiederherstellungs-Seite, um dich mit deinem PIN anzumelden.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -35,7 +35,15 @@
|
||||
clearTimer();
|
||||
if (paused) return;
|
||||
// Videos: advance on `ended` or after `max(dwell, 12s)` — whichever first.
|
||||
const ms = isVideo ? Math.max(dwellMs, 12000) : dwellMs;
|
||||
let ms = isVideo ? Math.max(dwellMs, 12000) : dwellMs;
|
||||
// Reduced-motion: slow auto-advance to a ≥30s floor so content doesn't
|
||||
// auto-change rapidly (WCAG 2.2.2); the CSS already snaps the transitions.
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
||||
) {
|
||||
ms = Math.max(ms, 30000);
|
||||
}
|
||||
advanceTimer = setTimeout(advance, ms);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user