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:
@@ -1268,7 +1268,7 @@ fn is_superseded_archive(
|
||||
/// want, since being wrong low means ENOSPC halfway through.
|
||||
///
|
||||
/// Matches [`query_uploads`]' visibility filter exactly, so hidden/banned uploads aren't counted.
|
||||
async fn estimate_export_bytes(pool: &PgPool, event_id: Uuid) -> Result<u64> {
|
||||
pub async fn estimate_export_bytes(pool: &PgPool, event_id: Uuid) -> Result<u64> {
|
||||
let (bytes,): (i64,) = sqlx::query_as(
|
||||
"SELECT COALESCE(SUM(u.original_size_bytes), 0)::bigint
|
||||
FROM upload u
|
||||
@@ -1302,6 +1302,20 @@ fn required_free_bytes(media_bytes: u64, armed: i64) -> u64 {
|
||||
needed.min(u64::MAX as u128) as u64
|
||||
}
|
||||
|
||||
/// Free bytes a full keepsake build would need RIGHT NOW, both halves included.
|
||||
///
|
||||
/// The same arithmetic the preflight uses, exposed so the host dashboard can warn BEFORE the
|
||||
/// release rather than reporting a failure after it. The preflight can only ever say "this didn't
|
||||
/// fit"; at that point the gallery is full, the event is over, and the remedies (ask guests to stop
|
||||
/// uploading, grow the volume) are all much harder. Hard-codes both halves because that is what a
|
||||
/// release arms.
|
||||
pub async fn keepsake_space_required(pool: &PgPool, event_id: Uuid) -> Result<u64> {
|
||||
Ok(required_free_bytes(
|
||||
estimate_export_bytes(pool, event_id).await?,
|
||||
2,
|
||||
))
|
||||
}
|
||||
|
||||
/// Refuse to start an export that cannot fit, with a reason the host can act on.
|
||||
///
|
||||
/// Without this the failure mode is ENOSPC halfway through a multi-GB write, and the wreckage
|
||||
|
||||
Reference in New Issue
Block a user