feat(disk): KEEPSAKE_ENABLED trades the archive for ~3x the usable media

The upload gate refuses any photo that would leave too little room to build the
keepsake, and the archive needs both halves at once — so it demands
`2.2 x media + 10 GB reserve + 1.5 GB headroom`. Every gigabyte of photos
therefore costs about 3.2 GB of disk budget, and usable media works out at
`(disk - 11.5 GB) / 3.2`. On a 30 GB volume that is only ~5.8 GB of photos.

Measured, not theorised: a 945-photo wedding (8.74 GB of real files) stopped
after ~780 photos with 24.98 GB free — the gate wanted 25.91 GB on a disk that
was 79% empty. Uploads ended for want of an archive nobody had asked for yet.

`KEEPSAKE_ENABLED=false` drops the archive term, leaving only `DISK_RESERVE_BYTES`
— the one question still live without an archive, which is whether Postgres can
still write. That turns the same 30 GB volume into ~20 GB of usable media.
Verified against the exact disk state that ended the run: the photo that returned
413 with the keepsake armed is accepted with it off.

The headroom term goes with it, deliberately. It exists only to keep this gate
strictly ahead of the EXPORT PREFLIGHT, and with no keepsake there is no
preflight to stay ahead of.

Boot-time immutable, like COMMENTS_ENABLED: flipping it mid-event would move the
gate under uploads already accepted against the old one. Releasing the gallery
and minting download tickets are refused while it is off, and the host dashboard
hides the release control rather than offering a button that answers 403 — a new
`keepsake_enabled` field on `GET /host/event` carries that.

The gate and the host's low-disk banner now share `upload_gate_required_free`.
They were already two expressions of one threshold, and the banner exists to fire
BEFORE the gate closes; applying the switch to only one of them would warn the
host about a limit that can no longer fire, or stay silent past one that can.
Tests pin the lead in both modes.

The cost is real and worth stating plainly: there is no downloadable gallery at
the end, so guests keep only what they save from the feed. Prefer a bigger disk
where you can — ~45 GB holds this library with the keepsake intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-08-20 22:57:25 +02:00
parent cacf616c2d
commit 464b270716
6 changed files with 238 additions and 35 deletions

View File

@@ -30,6 +30,8 @@
export_released: boolean;
disk_free_bytes: number | null;
keepsake_required_bytes: number;
/** False when KEEPSAKE_ENABLED=false — no downloadable gallery for this event. */
keepsake_enabled: boolean;
disk_low: boolean;
}
@@ -736,6 +738,11 @@
>
{event.uploads_locked ? 'Uploads wieder öffnen' : 'Uploads sperren'}
</button>
<!-- Hidden entirely when the keepsake is switched off (KEEPSAKE_ENABLED=false):
there is no archive to build, the backend refuses the call with 403, and a
button that always errors reads as a broken app rather than a disabled
feature. The note below says so in words instead. -->
{#if event.keepsake_enabled}
<button
onclick={() =>
(confirmAction = {
@@ -756,6 +763,12 @@
>
{event.export_released ? 'Galerie bereits freigegeben' : 'Galerie freigeben'}
</button>
{:else}
<p class="self-center text-sm text-gray-500 dark:text-gray-400">
Der Galerie-Download ist für dieses Event deaktiviert. Gäste speichern Fotos
direkt aus der Galerie.
</p>
{/if}
</div>
<!-- Live keepsake status: after release the ZIP/HTML still take time to build, and