Files
EventSnap/frontend/src/app.html
fabi a53729a704 fix(frontend): park uploads that cannot succeed, and stop two false signals
The upload queue gains `parkedFor`, so a photo rejected for a reason that cannot change on
its own stops re-pushing itself. A ban used to come back as a generic `forbidden`, which
purged the blob and moved the row to `blocked` — a terminal state with no retry button — so
lifting a ban restored everything except the photo actually in flight. Ban and release are
now distinct codes that keep the blob, charge no attempt, and tell the guest what has to
happen. `releaseResolvedParks` drains them at boot from /me/context, because the live
`user-shown` / `event-opened` events only reach a tab that was open when the host acted,
and the usual sequence is the other way round.

Two signals were firing on nothing. A filtered feed set `feedStale` on EVERY delta without
deduping — and the delta cursor boundary is inclusive while sse.ts deliberately rewinds
`lastEventTime`, so deltas routinely re-return rows already delivered. With the backstop
polling every 60-120s, a guest who tapped a hashtag got a "Neue Beiträge" pill they could
never clear, each tap costing a full filtered refetch. It now dedupes in both branches.

The SSE liveness backstop had the mirror problem: `noteDelivered` harvested id, upload_id
AND user_id from every payload, so by the time anything was deleted or anyone banned, their
ids were already marked delivered from ordinary traffic about live content. The
`deleted_ids` and `hidden_user_ids` clauses were false essentially always, leaving a
half-open socket undetected while a host moderated into a feed nobody was listening to.
Each event now records only the id its own clause tests.

Also: /admin no longer bounces to /join on a cleared session — AUTH_ROUTES had the `/admin`
prefix, which suppressed clearAuth() on the dashboard and let the login guard bounce back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 22:44:26 +02:00

224 lines
8.2 KiB
HTML

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<!-- viewport-fit=cover activates the env(safe-area-inset-*) padding used by the
bottom nav, sheets, toasts and FAB on notched / home-indicator devices. -->
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="text-scale" content="scale" />
<!-- Light/dark theme-color so the browser chrome matches the active theme. -->
<meta name="theme-color" content="#faf9f7" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#100f0f" media="(prefers-color-scheme: dark)" />
<!-- Installable PWA keepsake: manifest + Apple home-screen metadata. -->
<link rel="manifest" href="%sveltekit.assets%/manifest.webmanifest" />
<link rel="icon" href="%sveltekit.assets%/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="%sveltekit.assets%/icon.svg" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="EventSnap" />
<!--
FOUC guard: apply the dark class *before* paint, so reloads of pages with
theme=dark don't flash a white screen. Mirrors the logic in
`src/lib/theme-store.ts`; kept in sync by hand (it's 6 lines).
-->
<!-- nonce is required: our CSP sets script-src 'self', and SvelteKit's
mode:'auto' only hashes scripts *it* injects, not this template-authored
one. %sveltekit.nonce% is substituted per request and added to the CSP. -->
<script nonce="%sveltekit.nonce%">
(function () {
try {
var pref = localStorage.getItem('eventsnap_theme') || 'system';
var dark =
pref === 'dark' ||
(pref === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (dark) document.documentElement.classList.add('dark');
} catch (_) {}
// Colour-theme FOUC guard: re-apply the cached palette override before paint so
// a custom/preset theme doesn't flash the default gold. The value is a ready-to-
// inject `:root:root{…}` string from event-config-store (empty for the default
// theme); refreshed from /event once the app mounts.
try {
var css = localStorage.getItem('eventsnap_palette_css');
if (css) {
var s = document.createElement('style');
s.id = 'es-theme';
s.textContent = css;
document.head.appendChild(s);
}
} catch (_) {}
// Boot backstop. With `ssr = false` the page is EMPTY until the bundle mounts, so
// anything that stops it mounting leaves the guest on the spinner below forever:
// a chunk 404 after a redeploy, a dead uplink, or untranspiled syntax on an old
// phone. There is no message, no reload control, and in a standalone PWA no URL
// bar to escape from — the app is simply bricked for that guest, all evening.
//
// A TIMEOUT, deliberately, not feature detection: a SyntaxError in the bundle is
// invisible to any capability check, whereas "still not painted" catches every
// cause at once. The root layout removes #app-boot on mount, so its continued
// presence IS the failure signal and nothing needs to cancel this.
//
// 15s is well beyond a slow-but-healthy load on venue wifi (the bundle is ~130 kB
// gzipped); erring long matters more than erring short, because a false positive
// here would tell a guest something is broken while it is quietly working.
setTimeout(function () {
var boot = document.getElementById('app-boot');
if (!boot) return; // app mounted — nothing to do
// Classes, not inline styles. The panel must stay legible in dark mode, and
// the only stylesheet guaranteed to be present here is the inline <style>
// below — a boot failure includes the case where the app's own CSS 404'd
// too, so nothing may set a text colour. Inline styles cannot express the
// `html.dark` variant, and the first version's un-coloured heading rendered
// as the UA default black on the #100f0f dark background: invisible, on the
// one screen whose entire job is to be readable.
boot.innerHTML =
'<div class="app-boot__fail">' +
'<p class="app-boot__fail-title">Die App konnte nicht geladen werden</p>' +
'<p class="app-boot__fail-text">Bitte prüf deine Verbindung und lade die Seite neu.</p>' +
'<button id="app-boot-reload" class="app-boot__fail-btn">Neu laden</button>' +
'</div>';
var btn = document.getElementById('app-boot-reload');
if (btn)
btn.addEventListener('click', function () {
location.reload();
});
}, 15000);
})();
</script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
<!--
Cold-start placeholder. With `ssr = false` the app renders entirely client-side,
so `%sveltekit.body%` is empty until the JS bundle mounts. This themed spinner
(the FOUC guard above has already applied `.dark`) fills that gap instead of a
blank flash, and mirrors the /-route splash. The root layout's onMount removes
it once the app has painted. Inline <style> is permitted by our CSP
(style-src 'unsafe-inline'); intentionally no <script> here so script-src stays 'self'.
-->
<div id="app-boot" role="status" aria-label="Lädt">
<span class="app-boot__spinner"></span>
<span class="app-boot__label">EventSnap</span>
</div>
<!-- The app is client-rendered end to end, so with JS off there is nothing at all to
show. Say so, rather than leaving a permanent spinner over a blank page. -->
<noscript>
<div class="app-boot__noscript">
<p><strong>EventSnap braucht JavaScript</strong></p>
<p>Bitte aktiviere JavaScript im Browser und lade die Seite neu.</p>
</div>
</noscript>
<style>
#app-boot {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
background: #faf9f7;
}
html.dark #app-boot {
background: #100f0f;
}
.app-boot__spinner {
width: 2rem;
height: 2rem;
border-radius: 9999px;
border: 2px solid #e5e4e1;
border-top-color: #8a6a2b;
animation: app-boot-spin 0.6s linear infinite;
}
html.dark .app-boot__spinner {
border-color: #3b3a38;
border-top-color: #c6a24a;
}
.app-boot__label {
font-family: Georgia, 'Times New Roman', serif;
font-size: 1rem;
font-weight: 600;
letter-spacing: 0.01em;
color: #8a6a2b;
}
@keyframes app-boot-spin {
to {
transform: rotate(360deg);
}
}
/* Boot-failure panel, swapped in by the backstop timer above. Every colour is
stated explicitly in both themes: when this renders, the app's own stylesheet
may well be one of the things that failed to load. */
.app-boot__fail {
max-width: 20rem;
text-align: center;
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
}
.app-boot__fail-title {
margin: 0 0 0.5rem;
font-family: Georgia, 'Times New Roman', serif;
font-size: 1.125rem;
font-weight: 600;
color: #1c1b1a;
}
.app-boot__fail-text {
margin: 0 0 1.25rem;
line-height: 1.5;
color: #545350;
}
html.dark .app-boot__fail-title {
color: #f2f0ed;
}
html.dark .app-boot__fail-text {
color: #a6a4a1;
}
.app-boot__fail-btn {
font: inherit;
font-weight: 600;
cursor: pointer;
border: 0;
border-radius: 0.5rem;
padding: 0.625rem 1.25rem;
background: #8a6a2b;
color: #fff;
}
html.dark .app-boot__fail-btn {
background: #c6a24a;
color: #100f0f;
}
/* Sits above #app-boot, which is `position: fixed` and would otherwise cover it. */
.app-boot__noscript {
position: fixed;
inset: 0;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.25rem;
padding: 2rem;
text-align: center;
background: #faf9f7;
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
color: #545350;
}
html.dark .app-boot__noscript {
background: #100f0f;
color: #a6a4a1;
}
</style>
</body>
</html>