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:
@@ -20,6 +20,7 @@
|
||||
let expiry = $state<Date | null>(null);
|
||||
let pinCopied = $state(false);
|
||||
let leaveConfirmOpen = $state(false);
|
||||
let leaveEverywhere = $state(false);
|
||||
let dataModeWarningOpen = $state(false);
|
||||
|
||||
// `pin` is sourced from the shared `currentPin` store so a global pin-reset SSE
|
||||
@@ -99,10 +100,14 @@
|
||||
setTimeout(() => (pinCopied = false), 2000);
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
async function handleLogout(everywhere = false) {
|
||||
// Session-delete is best-effort: the JWT is going away on this device either way,
|
||||
// so a network failure shouldn't block the user from leaving the event.
|
||||
try { await api.delete('/session'); } catch { /* best-effort logout */ }
|
||||
// `everywhere` revokes ALL of this user's sessions (other phones/laptops recovered
|
||||
// via PIN), not just this device.
|
||||
try {
|
||||
await api.delete(everywhere ? '/sessions' : '/session');
|
||||
} catch { /* best-effort logout */ }
|
||||
// Wipe the IndexedDB upload queue so a second guest using the same device can't
|
||||
// inherit (or be blamed for) this guest's pending uploads. Not done on a 401
|
||||
// auto-clear — that path preserves the queue in case the user re-authenticates.
|
||||
@@ -379,9 +384,9 @@
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- Leave / logout -->
|
||||
<!-- Leave / logout (this device) -->
|
||||
<button
|
||||
onclick={() => (leaveConfirmOpen = true)}
|
||||
onclick={() => { leaveEverywhere = false; leaveConfirmOpen = true; }}
|
||||
class="flex w-full items-center gap-3 border-t border-gray-100 px-5 py-4 text-left transition hover:bg-red-50 dark:border-gray-700 dark:hover:bg-red-950/30"
|
||||
>
|
||||
<svg class="h-5 w-5 text-red-500 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
@@ -389,6 +394,17 @@
|
||||
</svg>
|
||||
<span class="flex-1 text-sm font-medium text-red-600 dark:text-red-400">Event verlassen</span>
|
||||
</button>
|
||||
|
||||
<!-- Sign out everywhere (all devices) -->
|
||||
<button
|
||||
onclick={() => { leaveEverywhere = true; leaveConfirmOpen = true; }}
|
||||
class="flex w-full items-center gap-3 border-t border-gray-100 px-5 py-4 text-left transition hover:bg-red-50 dark:border-gray-700 dark:hover:bg-red-950/30"
|
||||
>
|
||||
<svg class="h-5 w-5 text-red-500 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
|
||||
</svg>
|
||||
<span class="flex-1 text-sm font-medium text-red-600 dark:text-red-400">Auf allen Geräten abmelden</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -435,10 +451,12 @@
|
||||
<!-- Leave-confirm bottom sheet -->
|
||||
<ConfirmSheet
|
||||
open={leaveConfirmOpen}
|
||||
title="Event verlassen?"
|
||||
message="Du wirst abgemeldet. Mit deinem PIN kannst du jederzeit zurückkehren."
|
||||
title={leaveEverywhere ? 'Auf allen Geräten abmelden?' : 'Event verlassen?'}
|
||||
message={leaveEverywhere
|
||||
? 'Alle deine Sitzungen (auch auf anderen Geräten) werden beendet. Mit deinem PIN kannst du jederzeit zurückkehren.'
|
||||
: 'Du wirst abgemeldet. Mit deinem PIN kannst du jederzeit zurückkehren.'}
|
||||
confirmLabel="Abmelden"
|
||||
tone="danger"
|
||||
onConfirm={handleLogout}
|
||||
onConfirm={() => handleLogout(leaveEverywhere)}
|
||||
onCancel={() => (leaveConfirmOpen = false)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user