fix(upload): stop the queue from wedging, and tell the guest when it fails
A STALLED UPLOAD BLOCKED EVERYTHING, FOREVER. The XHR set no timeout and had no stall detection, so a half-open connection from an AP roam left the item `uploading` indefinitely — which kept `processQueue`'s `processing` flag set, so the whole rest of the queue stopped draining. The UI offered no control at all for an `uploading` item. The guest saw "Wird hochgeladen 43%" all evening with four photos stuck behind it and no button to press; the only escape was force-quitting the PWA, which nobody guesses. Now: a watchdog aborts when no bytes move for 90s, disarmed on `loadend` so the server may take its time storing a file it already has; a size-scaled timeout as a generous backstop that will not kill slow-but-progressing LTE; and a cancel button. FAILURES WERE INVISIBLE. `handleSubmit` navigates to /feed immediately, and the queue component is mounted only on /upload — so a 5xx, a captive-portal error or an uploads-locked 403 wrote a German message into an item that nothing ever rendered. The guest believed the photo was uploading; it never appeared. Same for the documented rate-limit countdown banner, which lives in that same unreachable component and is now also rendered from the layout. RETRIES WERE UNCAPPED. `requeueRetriable` flipped every errored item back to pending on the `online` event AND on every `feed-delta` — i.e. every SSE reconnect — with no attempt counter and no backoff. On a flapping network a large failing video was re-uploaded from byte zero all evening, saturating the AP for everyone. Now a persisted attempt count, exponential backoff and a cap of five. INDEXEDDB COULD STRAND THE COMPOSER. `openDB` had no `blocked` handler, so a second tab holding an older version made it never settle, and it rejects outright on iOS private mode; `handleSubmit` had no try/catch and never reset `submitting`, so both buttons stayed disabled reading "Wird hochgeladen…" permanently, with no error and nothing queued. There is now a `blocked` handler plus a settle timeout, an in-memory fallback so uploading still works when persistence is unavailable, and a `finally`. A 401 during a background upload cleared the session without redirecting — and api.ts documents exactly why that strands a guest: the nav and FAB are gated on `isAuthenticated` so they vanish, route guards only run on mount, and a standalone PWA has no URL bar. Three early-return paths wrote status only to memory and never to IndexedDB, leaving blob-less error rows that could never be evicted and held the red FAB badge lit all night. A banned guest was offered the entire upload flow — FAB, camera, staging — and only the POST 403'd, while the new read-only banner told them uploading was disabled. The sheet now consults the ban, the layout subscribes to `user-hidden` so a live ban reaches the UI instead of arriving as a stream of 403 toasts, and the banner clears `env(safe-area-inset-bottom)` so the bottom nav stops covering it on notched iPhones. The /upload submit bar gets the same inset — it sat in the home-indicator zone, where the system swallows the first tap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { api } from './api';
|
||||
import { setRole } from './role-store';
|
||||
import { privacyNote } from './privacy-note-store';
|
||||
import { isBanned } from './ban-store';
|
||||
import type { MeContextDto } from './types';
|
||||
|
||||
/**
|
||||
@@ -35,6 +37,13 @@ export async function refreshEventState(): Promise<void> {
|
||||
// The role is unaffected by the close/reopen race guarded below, so apply it
|
||||
// unconditionally — this is one of the refreshes that used to drop it.
|
||||
setRole(ctx.role);
|
||||
// Same reasoning, same bug shape: these two fields come back in the SAME payload and
|
||||
// were simply dropped on the floor here. `privacy_note` mattered most on the join
|
||||
// → feed path, where the layout's token-gated boot fetch never runs, so a first-time
|
||||
// guest's onboarding never learned the note existed. `is_banned` drives the read-only
|
||||
// banner, which has to appear the moment a ban lands, not on the next cold start.
|
||||
privacyNote.set(ctx.privacy_note);
|
||||
isBanned.set(ctx.is_banned);
|
||||
// A close/reopen landed while this was in flight — its result is now authoritative;
|
||||
// don't overwrite it with our possibly-stale snapshot.
|
||||
if (seq !== stateSeq) return;
|
||||
|
||||
Reference in New Issue
Block a user