fix(feed): survive a bad network, and let the lightbox actually browse
Four things a guest on congested venue wifi would have hit, and one they would have
hit immediately.
A FAILED FEED LOAD CLAIMED THE GALLERY WAS EMPTY. `loadFeed` caught, toasted for five
seconds and left `uploads` empty, so the page fell through to "Noch keine Fotos. Tippe
auf den Kamera-Button unten!" — the most likely first impression at the party, and a
lie. There is now a distinct error state with "Erneut laden". Refreshes suppressed the
toast entirely, so pull-to-refresh and the "Neue Beiträge" pill failed in total
silence; they now report, and the pill survives its own failure instead of clearing
before the request.
THE FILTER-EMPTY STATE WAS DEAD CODE. With filtering server-side `displayUploads` is a
plain alias of `uploads`, so the grid's "Keine Treffer für die gewählten Filter." plus
its reset button sat behind an identical earlier branch and could never render — a
guest tapping a chip with no matches was told to go take a photo.
SSE COULD FREEZE THE FEED FOR THE WHOLE EVENING. Nothing in the feed ever refetched on
a timer; every update path was triggered exclusively by a stream event. Behind a proxy
that buffers `text/event-stream` `onopen` never fires, so the guest saw only the photos
that were on screen when they arrived; and a socket left half-open by an AP roam is
worse, because `connectSse` early-returns on a non-null EventSource and nothing ever
reconnects. A pure silence timer is not implementable — the backend sends keep-alives
as SSE comments, which the EventSource parser discards without dispatching — so
liveness is established on evidence instead: a jittered 60-120s `/feed/delta` backstop
that reconnects when a poll returns content the stream never delivered. The ticket
round-trip also seeds the delta cursor before the EventSource is created, so the
backstop has a `since` even if `onopen` never fires.
THE PILL COLLAPSED A DEEPLY-SCROLLED FEED to 20 items and dumped the guest at an
arbitrary scroll position — the exact yank the pill exists to avoid. It merges now.
The refresh debounce was 800ms + jitter, which during a burst is roughly one feed query
per client every two seconds; at 100 guests that approaches the 60/min per-user limit,
and the resulting 429s were swallowed by a bare `catch {}`, so the feed would simply
stop updating with no signal. Now 8s + jitter, coalescing, and skipped entirely while
the page is hidden.
Not one `<img>` in the app had an `onerror`. `pickMediaUrl` falls back to the original
whenever preview and thumbnail are null — i.e. for everything still compressing, which
during a burst is the top of the feed — so a 404 there rendered an empty grey box with
`alt=""`, not even a message. Each now retries once, then shows the placeholder.
The lightbox had no swipe, no prev/next and no arrow keys, so browsing 300 photos meant
closing and reopening the modal for every one — while FEATURES.md and USER_JOURNEYS
both claimed swipe shipped. It now has chevrons (44px, German aria-labels, hidden at
the ends), arrow keys, and horizontal swipe, with focus handed to the surviving control
so a disappearing chevron can't drop focus to `<body>`. Comment deletion was a ~14px
`✕` four pixels from the text that deleted permanently on one tap, while deleting a
POST two components away goes through a ConfirmSheet; it now matches.
`feed-filter.ts` and its test are deleted — with the server filtering, they were dead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
* paint the right colours before the JS bundle loads (no flash of the default palette).
|
||||
*/
|
||||
import { writable } from 'svelte/store';
|
||||
import { api } from './api';
|
||||
import { buildPaletteCss, applyPaletteCss, type ThemeConfig } from './theme/palette';
|
||||
|
||||
export const PALETTE_CACHE_KEY = 'eventsnap_palette_css';
|
||||
@@ -23,12 +24,24 @@ export const eventConfig = writable<EventConfig | null>(null);
|
||||
/** Convenience flag for the comment UI. Optimistic `true` until /event resolves. */
|
||||
export const commentsEnabled = writable<boolean>(true);
|
||||
|
||||
type PublicEventDto = {
|
||||
name: string;
|
||||
slug: string;
|
||||
comments_enabled?: boolean;
|
||||
theme_preset?: string;
|
||||
theme_primary?: string;
|
||||
theme_accent?: string;
|
||||
};
|
||||
|
||||
/** Fetch /event, apply the theme, and cache the resolved CSS for the next boot. */
|
||||
export async function loadEventConfig(): Promise<void> {
|
||||
try {
|
||||
const res = await fetch('/api/v1/event');
|
||||
if (!res.ok) return;
|
||||
const b = await res.json();
|
||||
// Through `api` rather than a bare `fetch`: this was the ONE request in the app with
|
||||
// no AbortController deadline. On a congested venue WiFi a socket that never answers
|
||||
// left the promise pending indefinitely, so the palette never reconciled with the
|
||||
// server — and since `event-updated` re-calls this, those zombie requests stack up.
|
||||
// `api` aborts at 20s and turns it into an ApiError the catch below already handles.
|
||||
const b = await api.get<PublicEventDto>('/event');
|
||||
const theme: ThemeConfig = {
|
||||
preset: b.theme_preset ?? 'champagne-gold',
|
||||
primary: b.theme_primary ?? '#8a6a2b',
|
||||
|
||||
Reference in New Issue
Block a user