fix(user-flow): close offline-queue, export, session, ban & realtime gaps
Comprehensive user-flow review across guest/host/admin roles, then fixes with
e2e regression guards. Highlights:
- Offline upload queue auto-resumes on reconnect: network errors keep items
pending (not error), 4xx are terminal (no infinite retry), quota exhaustion
returns a distinct 413; queue cap + dedup.
- Export lifecycle: release atomically locks uploads; reopen invalidates and
re-release regenerates the keepsake; workers claim their job atomically so a
reopen->re-release can't corrupt the ZIP; startup re-spawns interrupted
exports.
- Sessions slide on activity (no 30-day cliff); JWT expiry deferred to the
revocable session row; sign-out-everywhere + revoke-on-PIN-reset.
- Ban always hides content (v_feed / find_visible_media / export filter
is_banned) but stays a read-only ban per USER_JOURNEYS §10 — sessions are
not revoked, read access + keepsake download preserved.
- Realtime: server-clock SSE delta cursor; event-closed/opened drive the UI
live; feed_delta rate-limited; like returns {liked, like_count} to fix
multi-device drift; lightbox live comments; diashow delta backfill.
- Forgotten-PIN in-app request flow; simultaneous same-name join returns 409;
quota increment is transactional; operator floor.
Adds e2e/specs/10-flow-review/ (offline resume, export integrity, deterministic
anti-race guard) and updates existing specs for the new contracts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,9 +58,21 @@
|
||||
} catch { /* ignore */ }
|
||||
});
|
||||
|
||||
// Pull in a comment posted from another device on the CURRENTLY-open upload, so the
|
||||
// open list matches the count. The `new-comment` broadcast carries only the count (not
|
||||
// the body), so refetch — skipped when it's for a different upload, and the dedupe
|
||||
// below makes our own just-posted optimistic comment a no-op.
|
||||
const unsubNewComment = onSseEvent('new-comment', (data) => {
|
||||
try {
|
||||
const { upload_id } = JSON.parse(data) as { upload_id: string };
|
||||
if (upload_id === upload.id) void loadComments(upload.id);
|
||||
} catch { /* ignore */ }
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (burstTimer) clearTimeout(burstTimer);
|
||||
unsubCommentDeleted();
|
||||
unsubNewComment();
|
||||
});
|
||||
|
||||
// Only refetch when a *different* upload is shown. The feed reassigns the
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
case 'uploading': return 'Wird hochgeladen';
|
||||
case 'done': return 'Fertig';
|
||||
case 'error': return 'Fehler';
|
||||
case 'blocked': return 'Gesperrt';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +24,7 @@
|
||||
case 'uploading': return 'text-blue-600 dark:text-blue-400';
|
||||
case 'done': return 'text-green-600 dark:text-green-400';
|
||||
case 'error': return 'text-red-600 dark:text-red-400';
|
||||
case 'blocked': return 'text-red-600 dark:text-red-400';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +96,7 @@
|
||||
Erneut
|
||||
</button>
|
||||
{/if}
|
||||
{#if item.status === 'done' || item.status === 'error'}
|
||||
{#if item.status === 'done' || item.status === 'error' || item.status === 'blocked'}
|
||||
<button
|
||||
onclick={() => removeItem(item.id)}
|
||||
class="inline-flex h-9 w-9 items-center justify-center text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
import { scrollLock } from '$lib/actions/scroll-lock';
|
||||
import CameraCapture from '$lib/components/CameraCapture.svelte';
|
||||
import type { PendingFile } from '$lib/pending-upload-store';
|
||||
import { eventState, uploadsClosed } from '$lib/event-state-store';
|
||||
|
||||
// Uploads closed (event locked or gallery released) — show a lock notice instead of
|
||||
// the capture options, so a guest can't stage a photo that would just be rejected.
|
||||
let closed = $derived(uploadsClosed($eventState));
|
||||
|
||||
let showCamera = $state(false);
|
||||
let fileInput: HTMLInputElement;
|
||||
@@ -149,6 +154,22 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 px-4 pb-4 pt-2">
|
||||
{#if closed}
|
||||
<!-- Uploads closed: no capture options, just an explanation + dismiss. -->
|
||||
<div class="rounded-xl bg-amber-50 px-5 py-4 text-center dark:bg-amber-950/30">
|
||||
<p class="font-semibold text-amber-800 dark:text-amber-300">Uploads geschlossen</p>
|
||||
<p class="mt-1 text-sm text-amber-700 dark:text-amber-400">
|
||||
Der Host hat die Uploads für dieses Event beendet. Du kannst weiterhin Fotos ansehen,
|
||||
liken und kommentieren.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={close}
|
||||
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-600 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
{:else}
|
||||
<!-- Gallery option -->
|
||||
<button
|
||||
onclick={openGallery}
|
||||
@@ -189,5 +210,6 @@
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user