The two-phase preflight from eb0e405 landed in ONE place and was spliced inside
the other. `run_zip_export` ended up containing both blocks nested, so the
Gallery path pruned "Memories" archives that were not its to reclaim, while
`run_html_export` silently kept the single-phase form.
That left the exact deadlock the two-phase preflight exists to break, still
open on half the product. At a gallery size where a rebuild needs the previous
generation's bytes: the ZIP prunes its own superseded archive and rebuilds, and
the HTML preflight fails against a Memories archive still on disk. The prune
that would free it runs only after a success that can never happen, and any
epoch bump — a guest deleting one photo — retires the current viewer
immediately. Permanently stuck, unreachable from any handler, discovered at the
end of the night with nobody there.
Both halves now call one `ensure_export_space_reclaiming`, keyed on the
caller's OWN prefix, so they cannot drift again.
Three smaller things found in the same pass:
- The boot-failure panel hardcoded light-mode colours, and its heading set none
at all — the UA default black on the `#100f0f` dark background. On the one
screen whose entire job is to be readable, and in the failure mode where the
app's own stylesheet may be what did not load. Moved to classes in the inline
<style> so the `html.dark` variants apply.
- `.env.example` assigned RUST_LOG twice, 120 lines apart. Compose takes the
last one, so an operator raising the level mid-event to chase a problem would
have changed nothing, silently.
- The comment justifying `detail = ?message` in error.rs still claimed
`validate_display_name` allows newlines. It rejects control characters now —
but that is one input against every 4xx message in the app, so the escaping
is what makes the guarantee general. Said so.
151/151 backend, 58/58 vitest, clippy clean, svelte-check 0 errors, both
builds, caddy validate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
211 lines
8.1 KiB
HTML
211 lines
8.1 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>
|