feat(host): warn about low disk before it becomes unrecoverable
Storage visibility existed in exactly one place: a passive Speicherauslastung widget on the ADMIN dashboard. A host who isn't the admin had no view of it, and nothing warned anyone. README carried "Low-disk alert (< 10 GB free)" under Planned since v1. Two things make this a safety net rather than a nice-to-have. postgres_data, media_data and exports_data are all Docker named volumes on ONE filesystem, so running out doesn't degrade a subsystem -- Postgres stops being able to write and the whole event goes down. And the keepsake needs room for two gallery-sized archives, which the export preflight can only ever refuse AFTER the release, when the event is over and every remedy is harder. So the threshold is not a fixed number alone. It fires on the 10 GB floor the README always named, OR on "you could not build the keepsake right now" -- the trigger a host can still act on, computed with the same arithmetic the preflight uses. Unknown free space is NOT low: it fails open like the upload quota and the preflight do, because a banner that cries wolf on an unreadable mount is a banner nobody reads. Carried on GET /host/event, which the dashboard already fetches on load and on every reload -- no new endpoint, no new poll. Rendered above everything else including the PIN-reset queue, and it names the consequence (the event, not just the download) rather than only the number. Also fixes the host page's formatBytes, which topped out at MB: 30 GB free would have rendered as "30720.0 MB", and a guest with 2 GB of uploads was already being shown that way in the user list. Tests: 5 unit on the threshold (including that plenty of free space is still low when the keepsake wouldn't fit -- the case a fixed threshold misses entirely), 3 e2e. The e2e drives it through `original_size_bytes` rather than a genuinely full disk: the estimate is pure SQL over that column, so overstating one row moves the accounting without touching a byte on disk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,9 @@
|
||||
is_active: boolean;
|
||||
uploads_locked: boolean;
|
||||
export_released: boolean;
|
||||
disk_free_bytes: number | null;
|
||||
keepsake_required_bytes: number;
|
||||
disk_low: boolean;
|
||||
}
|
||||
|
||||
interface PinResetRequest {
|
||||
@@ -395,7 +398,11 @@
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
// GB matters here now that this also renders free disk and keepsake size — the previous
|
||||
// version topped out at MB, so 30 GB free read as "30720.0 MB" (and a guest with 2 GB of
|
||||
// uploads was already being rendered the same way in the user list).
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -507,6 +514,38 @@
|
||||
{error}
|
||||
</div>
|
||||
{:else if event}
|
||||
<!-- ── Speicherwarnung ─────────────────────────────────────────────
|
||||
Above everything else on purpose. All three volumes (postgres_data, media_data,
|
||||
exports_data) sit on one filesystem, so running out doesn't degrade a subsystem —
|
||||
it stops Postgres writing and takes the event down. And the keepsake needs room
|
||||
for TWO gallery-sized archives, which is only actionable BEFORE the release: the
|
||||
export preflight can say "this didn't fit", but by then the event is over and the
|
||||
remedies are all much harder.
|
||||
|
||||
Only the admin dashboard had any storage visibility at all, and a host is often
|
||||
not the admin. `disk_low` fails closed to "not low" on an unreadable mount, so
|
||||
this cannot cry wolf. -->
|
||||
{#if event.disk_low && event.disk_free_bytes !== null}
|
||||
<div
|
||||
class="rounded-xl border border-red-300 bg-red-50 p-4 dark:border-red-800 dark:bg-red-950/30"
|
||||
data-testid="low-disk-warning"
|
||||
>
|
||||
<h2 class="font-semibold text-red-900 dark:text-red-200">Speicherplatz wird knapp</h2>
|
||||
<p class="mt-1 text-sm text-red-800 dark:text-red-300">
|
||||
Noch <strong>{formatBytes(event.disk_free_bytes)}</strong> frei.
|
||||
{#if event.keepsake_required_bytes > event.disk_free_bytes}
|
||||
Für das Keepsake werden derzeit ca.
|
||||
<strong>{formatBytes(event.keepsake_required_bytes)}</strong> benötigt — es kann
|
||||
momentan <strong>nicht</strong> erstellt werden.
|
||||
{/if}
|
||||
</p>
|
||||
<p class="mt-1.5 text-xs text-red-700 dark:text-red-400">
|
||||
Schaffe Speicher frei oder vergrößere den Datenträger. Wenn der Datenträger vollläuft,
|
||||
fällt das gesamte Event aus — nicht nur der Download.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- ── PIN-Reset-Anfragen ──────────────────────────────────────── -->
|
||||
{#if pinResetRequests.length > 0}
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user