fix(export): give the keepsake viewer the two-phase preflight it was meant to get
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>
This commit is contained in:
@@ -99,12 +99,15 @@ impl IntoResponse for AppError {
|
||||
//
|
||||
// * `message` is tracing's own reserved field for an event's format literal, so `%message`
|
||||
// printed unlabelled and would collide under a JSON layer.
|
||||
// * Debug formatting QUOTES AND ESCAPES the string. `validate_display_name` allows
|
||||
// newlines (it rejects only NUL and length), and several 4xx messages interpolate the
|
||||
// guest's chosen name — `Der Name "X" ist bereits vergeben.` So with Display
|
||||
// formatting, two unauthenticated `/join` requests could forge arbitrary lines in the
|
||||
// only forensic record an unattended event has: pick a name containing a newline and a
|
||||
// plausible log prefix, then trigger the 409. Escaping closes that.
|
||||
// * Debug formatting QUOTES AND ESCAPES the string, and several 4xx messages interpolate
|
||||
// attacker-chosen text — the guest's name in `Der Name "X" ist bereits vergeben.`, and
|
||||
// multipart/parse errors that echo their input. With Display formatting, a value
|
||||
// carrying a newline plus a plausible log prefix lets two unauthenticated requests
|
||||
// forge lines in the only forensic record an unattended event has.
|
||||
// `validate_display_name` now rejects control characters, so the name route is closed
|
||||
// at the source as well — but that is ONE input, and this line formats every 4xx
|
||||
// message in the app. Escaping here is what makes the guarantee general; do not
|
||||
// "simplify" it to `%message` on the grounds that names are already validated.
|
||||
//
|
||||
// 401 and 404 are logged at DEBUG rather than WARN. They carry no operator signal (an
|
||||
// expired session, a mistyped URL) and they are the cheapest lines for a scanner to
|
||||
|
||||
Reference in New Issue
Block a user