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,
|
// Mirrors the three clauses in `carried`: uploads by upload id, deletions by upload id,
|
||||||
// ban-hides by user id.
|
// ban-hides by user id.
|
||||||
const relevant =
|
const relevant =
|
||||||
eventName === 'new-upload' || eventName === 'upload-processed'
|
eventName === 'new-upload'
|
||||||
? p.id
|
? 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)
|
? (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
|
? p.user_id
|
||||||
: undefined;
|
: undefined;
|
||||||
if (typeof relevant === 'string') rememberDelivered(relevant);
|
if (typeof relevant === 'string') rememberDelivered(relevant);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
import { privacyNote } from '$lib/privacy-note-store';
|
import { privacyNote } from '$lib/privacy-note-store';
|
||||||
import { refreshQuota } from '$lib/quota-store';
|
import { refreshQuota } from '$lib/quota-store';
|
||||||
import { onSseEvent } from '$lib/sse';
|
import { onSseEvent } from '$lib/sse';
|
||||||
import { api } from '$lib/api';
|
import { api, ApiError } from '$lib/api';
|
||||||
import type { MeContextDto } from '$lib/types';
|
import type { MeContextDto } from '$lib/types';
|
||||||
import { eventState, markClosed, markOpened, refreshEventState } from '$lib/event-state-store';
|
import { eventState, markClosed, markOpened, refreshEventState } from '$lib/event-state-store';
|
||||||
import { setRole } from '$lib/role-store';
|
import { setRole } from '$lib/role-store';
|
||||||
@@ -104,9 +104,44 @@
|
|||||||
banned: ctx.is_banned,
|
banned: ctx.is_banned,
|
||||||
uploadsOpen: !ctx.uploads_locked && !ctx.gallery_released
|
uploadsOpen: !ctx.uploads_locked && !ctx.gallery_released
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (err) {
|
||||||
// Cross-cutting hydration on boot — failure is non-fatal; users without
|
// Cross-cutting hydration on boot — failure is non-fatal; users without
|
||||||
// a session land on /join anyway, and the per-page mount will retry.
|
// 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();
|
void refreshQuota();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user