fix(frontend): stop a parked photo being stranded for the session, and fix an SSE id
A parked upload has three ways to be released, and two were weaker than the toast that promises "wird gesendet, sobald die Sperre aufgehoben ist": * The live user-shown / event-opened events only reach a tab with an open stream, and streams are opened by /feed, /diashow, /export, /host and /admin — NOT /upload, which is exactly where the toast sends the guest to watch their queue. * The boot-time release ran once and swallowed any failure, so a single failed request on venue wifi — the condition the whole parking mechanism exists for — skipped it for the entire session, leaving the row reading "Du bist gesperrt." after the ban was long lifted. Now retried once. Deliberately NOT on a 401: api.get already answered that by clearing auth and redirecting to /join, so a second attempt can only fire a second redirect two seconds later, by which time the guest may have navigated away. (That is not hypothetical — it made an existing browser-chaos spec fail while I was writing this.) Guarded rather than an early return, so the SSE listener registrations below still run. Also: noteDelivered mapped upload-processed to p.id, but that payload carries upload_id, so it recorded nothing — the docstring claimed a property the code did not have. And it recorded user-shown against a `carried` clause that only ever means "hidden", where it could only suppress a later genuine signal.
This commit is contained in:
@@ -367,11 +367,18 @@ function noteDelivered(eventName: string, data: string): void {
|
||||
// Mirrors the three clauses in `carried`: uploads by upload id, deletions by upload id,
|
||||
// ban-hides by user id.
|
||||
const relevant =
|
||||
eventName === 'new-upload' || eventName === 'upload-processed'
|
||||
eventName === 'new-upload'
|
||||
? p.id
|
||||
: eventName === 'upload-deleted'
|
||||
: // `upload-processed` carries `upload_id`, not `id` (see compression.rs) — reading
|
||||
// `p.id` recorded nothing at all, so this branch quietly did the opposite of what
|
||||
// the comment above claims. Harmless today only because the delta cursor is
|
||||
// anchored by `new-upload`, which is not a property worth depending on.
|
||||
eventName === 'upload-processed' || eventName === 'upload-deleted'
|
||||
? (p.upload_id ?? p.id)
|
||||
: eventName === 'user-hidden' || eventName === 'user-shown'
|
||||
: // Only `user-hidden` has a matching clause in `carried` (`hidden_user_ids` comes
|
||||
// from `uploads_hidden = TRUE`). Recording an UNBAN's user id could only ever
|
||||
// suppress a later genuine signal, so it is deliberately not recorded.
|
||||
eventName === 'user-hidden'
|
||||
? p.user_id
|
||||
: undefined;
|
||||
if (typeof relevant === 'string') rememberDelivered(relevant);
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { privacyNote } from '$lib/privacy-note-store';
|
||||
import { refreshQuota } from '$lib/quota-store';
|
||||
import { onSseEvent } from '$lib/sse';
|
||||
import { api } from '$lib/api';
|
||||
import { api, ApiError } from '$lib/api';
|
||||
import type { MeContextDto } from '$lib/types';
|
||||
import { eventState, markClosed, markOpened, refreshEventState } from '$lib/event-state-store';
|
||||
import { setRole } from '$lib/role-store';
|
||||
@@ -104,9 +104,44 @@
|
||||
banned: ctx.is_banned,
|
||||
uploadsOpen: !ctx.uploads_locked && !ctx.gallery_released
|
||||
});
|
||||
} catch {
|
||||
} catch (err) {
|
||||
// Cross-cutting hydration on boot — failure is non-fatal; users without
|
||||
// a session land on /join anyway, and the per-page mount will retry.
|
||||
//
|
||||
// But ONE consequence is not recoverable by a per-page mount: the park release
|
||||
// above. A parked photo's other two release paths are the live `user-shown` /
|
||||
// `event-opened` SSE events, and the routes that open a stream are /feed, /diashow,
|
||||
// /export, /host and /admin — NOT /upload, which is exactly where the toast sends
|
||||
// the guest to watch their queue. So on venue wifi, the condition this branch
|
||||
// exists for, one failed request could strand the photo for the whole session with
|
||||
// the queue row still reading "Du bist gesperrt.". Retry once, briefly.
|
||||
//
|
||||
// NOT on a 401. `api.get` already answered that one by calling `clearAuth()` and
|
||||
// redirecting to /join, so there is no session left to hydrate and a second attempt
|
||||
// can only fire a SECOND redirect — two seconds later, by which time the guest may
|
||||
// have navigated somewhere else. Retry the transient case this exists for (offline,
|
||||
// 5xx, a dropped request) and nothing else.
|
||||
//
|
||||
// A guard, not an early `return`: everything below this block — `refreshQuota`
|
||||
// and, outside it, every SSE listener registration — still has to run.
|
||||
const worthRetrying = !(err instanceof ApiError && err.status === 401);
|
||||
if (worthRetrying) {
|
||||
try {
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
const ctx = await api.get<MeContextDto>('/me/context');
|
||||
isBanned.set(ctx.is_banned);
|
||||
eventState.set({
|
||||
uploadsLocked: ctx.uploads_locked,
|
||||
galleryReleased: ctx.gallery_released
|
||||
});
|
||||
void releaseResolvedParks({
|
||||
banned: ctx.is_banned,
|
||||
uploadsOpen: !ctx.uploads_locked && !ctx.gallery_released
|
||||
});
|
||||
} catch {
|
||||
// Still down. The "Erneut" button on the parked row remains the way back.
|
||||
}
|
||||
}
|
||||
}
|
||||
void refreshQuota();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user