From fffa2d556c6fb6e44fcc6b7f5a54b0646e962283 Mon Sep 17 00:00:00 2001 From: "Fabian Hamm (Privat)" Date: Mon, 3 Aug 2026 18:36:30 +0200 Subject: [PATCH] fix(upload): stop the queue from wedging, and tell the guest when it fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/lib/ban-store.ts | 18 + .../src/lib/components/UploadQueue.svelte | 22 + .../src/lib/components/UploadSheet.svelte | 90 ++- frontend/src/lib/event-state-store.ts | 9 + frontend/src/lib/onboarding.ts | 45 ++ frontend/src/lib/upload-queue.ts | 666 +++++++++++++++--- frontend/src/routes/+layout.svelte | 99 ++- frontend/src/routes/upload/+page.svelte | 87 ++- 8 files changed, 915 insertions(+), 121 deletions(-) create mode 100644 frontend/src/lib/ban-store.ts create mode 100644 frontend/src/lib/onboarding.ts diff --git a/frontend/src/lib/ban-store.ts b/frontend/src/lib/ban-store.ts new file mode 100644 index 0000000..e149e4c --- /dev/null +++ b/frontend/src/lib/ban-store.ts @@ -0,0 +1,18 @@ +// Whether the CURRENT guest is banned. +// +// The ban is deliberately read-only (see backend `handlers/host.rs`): the guest keeps the feed +// and the released keepsake, but every write — upload, like, comment, delete — is refused with +// 403 "Du bist gesperrt.". Until this store existed the client had no idea, so all of those +// controls rendered fully enabled and the guest discovered the ban one error toast at a time, +// with nobody at the party to ask. The event-lock case has always been surfaced live for +// exactly this reason (`event-state-store`); a ban simply never was. +// +// Seeded from `/me/context` (root layout on boot, and `refreshEventState` afterwards). Reset on +// identity change so a shared device does not carry one guest's ban to the next. + +import { writable } from 'svelte/store'; +import { onClearAuth } from './auth'; + +export const isBanned = writable(false); + +onClearAuth(() => isBanned.set(false)); diff --git a/frontend/src/lib/components/UploadQueue.svelte b/frontend/src/lib/components/UploadQueue.svelte index ac63cd1..167effc 100644 --- a/frontend/src/lib/components/UploadQueue.svelte +++ b/frontend/src/lib/components/UploadQueue.svelte @@ -4,6 +4,7 @@ isProcessing, retryItem, removeItem, + cancelItem, clearCompleted, rateLimitRetryAt } from '$lib/upload-queue'; @@ -121,6 +122,27 @@ Erneut {/if} + + {#if item.status === 'uploading'} + + {/if} {#if item.status === 'done' || item.status === 'error' || item.status === 'blocked'}