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>
This commit is contained in:
@@ -27,6 +27,9 @@
|
||||
let refreshing = $state(false);
|
||||
let pullProgress = $state(0); // 0–1+ during the drag, 0 when idle
|
||||
let selectedUpload = $state<FeedUpload | null>(null);
|
||||
// Set when a truncated feed-delta means we missed too much to merge — shows a
|
||||
// tap-to-refresh pill instead of yanking the user's scroll to page 1.
|
||||
let feedStale = $state(false);
|
||||
let sentinel: HTMLDivElement;
|
||||
let feedObserver: IntersectionObserver | null = null;
|
||||
let inPlaceRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -196,6 +199,28 @@
|
||||
if (selectedUpload?.id === payload.upload_id) selectedUpload = null;
|
||||
} catch { /* ignore */ }
|
||||
}),
|
||||
// A background transcode failed: the backend already cleaned up (refunded
|
||||
// quota, removed the row) and an upload-deleted evicts the card. Only the
|
||||
// uploader gets a toast — registered before upload-deleted so the card is
|
||||
// still present to check ownership.
|
||||
onSseEvent('upload-error', (data) => {
|
||||
try {
|
||||
const { upload_id } = JSON.parse(data) as { upload_id: string };
|
||||
const mine = uploads.find((u) => u.id === upload_id && u.user_id === myUserId);
|
||||
if (mine) {
|
||||
toast('Ein Upload konnte nicht verarbeitet werden.', 'error');
|
||||
void refreshQuota();
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}),
|
||||
// A banned user's uploads were hidden — drop all their cards live.
|
||||
onSseEvent('user-hidden', (data) => {
|
||||
try {
|
||||
const { user_id } = JSON.parse(data) as { user_id: string };
|
||||
uploads = uploads.filter((u) => u.user_id !== user_id);
|
||||
if (selectedUpload && selectedUpload.user_id === user_id) selectedUpload = null;
|
||||
} catch { /* ignore */ }
|
||||
}),
|
||||
// Patch the single affected card in place from the SSE payload instead of
|
||||
// refetching page 1 — a busy event fires these constantly and a full reload
|
||||
// would yank every scrolled-down user back to the top on each reaction.
|
||||
@@ -210,7 +235,9 @@
|
||||
// Missed more than the backend delta cap while backgrounded — the
|
||||
// delta is only the newest slice, so merging would leave a silent gap
|
||||
// of older-but-still-new uploads. Resync from page 1 instead.
|
||||
void loadFeed(true);
|
||||
// Rather than involuntarily yank the user to page 1, surface a
|
||||
// tap-to-refresh pill so they keep scroll until they resync.
|
||||
feedStale = true;
|
||||
return;
|
||||
}
|
||||
if (delta.uploads.length) {
|
||||
@@ -440,6 +467,17 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if feedStale}
|
||||
<div class="pointer-events-none fixed left-0 right-0 top-[calc(env(safe-area-inset-top)+0.5rem)] z-40 flex justify-center">
|
||||
<button
|
||||
type="button"
|
||||
class="pointer-events-auto rounded-full bg-blue-600 px-4 py-1.5 text-xs font-semibold text-white shadow-lg hover:bg-blue-700"
|
||||
onclick={() => { feedStale = false; void loadFeed(true); }}
|
||||
>
|
||||
Neue Beiträge – tippen zum Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Sticky header — opaque fallback for browsers without backdrop-filter. -->
|
||||
<div class="sticky top-0 z-30 border-b border-gray-200 bg-white/95 pt-[env(safe-area-inset-top)] backdrop-blur supports-[not(backdrop-filter:blur(0))]:bg-white dark:border-gray-800 dark:bg-gray-900/95 dark:supports-[not(backdrop-filter:blur(0))]:bg-gray-900">
|
||||
<div class="mx-auto flex max-w-2xl items-center justify-between px-4 py-3">
|
||||
|
||||
Reference in New Issue
Block a user