/** * Regression guard — the host is warned about storage BEFORE it becomes unrecoverable. * * Storage visibility used to exist in exactly one place: a passive "Speicherauslastung" widget on * the ADMIN dashboard. A host who isn't the admin had no view of it at all, and nothing anywhere * warned anyone. README listed a low-disk alert under "Planned (v1.x)". * * Two things make that a safety net rather than a nice-to-have: * * - `postgres_data`, `media_data` and `exports_data` are all Docker named volumes on ONE * filesystem. A full disk doesn't degrade a subsystem; Postgres stops being able to write and * the whole event goes down. * - The keepsake needs room for TWO gallery-sized archives (both write their media * `Compression::Stored`; `Memories.zip` streams the original for every video and every image * at or under 5 MB). The export preflight can refuse cleanly, but only AFTER the release — * when the event is over, the gallery is full, and every remedy is harder. * * So the threshold is deliberately NOT a fixed number alone. It fires on an absolute floor (10 GB, * the figure the README always carried) OR on "you could not build the keepsake right now", which * is the trigger a host can still act on. * * These drive 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 the warning reads without * touching a byte on disk. */ import { test, expect } from '../../fixtures/test'; import { seedUpload } from '../../helpers/seed'; import { BASE } from '../../helpers/env'; /** Comfortably larger than any disk this suite could run on. */ const ABSURD_BYTES = 500_000_000_000_000; test.describe('Host — low-disk warning', () => { test('a gallery too big to export warns the host, with the numbers', async ({ page, host, guest, signIn, db, }) => { const g = await guest('BigShooter'); const uploadId = await seedUpload(g.jwt); await db.setUploadSizeBytes(uploadId, ABSURD_BYTES); await signIn(page, host); await page.goto('/host'); const warning = page.getByTestId('low-disk-warning'); await expect(warning, 'the host must be warned before releasing').toBeVisible({ timeout: 15_000, }); // The actionable half: not just "low", but "the keepsake cannot be built". await expect(warning).toContainText(/nicht.*erstellt werden/i); // And the consequence that makes it urgent — the event, not just the download. await expect(warning).toContainText(/gesamte Event/i); }); test('the API reports the requirement and the verdict together', async ({ host, guest, db }) => { const g = await guest('BigShooter2'); const uploadId = await seedUpload(g.jwt); await db.setUploadSizeBytes(uploadId, ABSURD_BYTES); const res = await fetch(`${BASE}/api/v1/host/event`, { headers: { Authorization: `Bearer ${host.jwt}` }, }); expect(res.status).toBe(200); const body = (await res.json()) as { disk_low: boolean; disk_free_bytes: number | null; keepsake_required_bytes: number; }; expect(body.disk_low).toBe(true); expect( body.keepsake_required_bytes, 'both halves are armed by a release, so the requirement covers two archives' ).toBeGreaterThan(ABSURD_BYTES); expect(body.disk_free_bytes).not.toBeNull(); expect(body.keepsake_required_bytes).toBeGreaterThan(body.disk_free_bytes!); }); test('an ordinary gallery shows no warning at all', async ({ page, host, guest, signIn }) => { // The mirror that keeps the above honest. A warning that is always on is a warning nobody // reads — and it would sit at the very top of the dashboard, above the PIN-reset queue. const g = await guest('NormalShooter'); await seedUpload(g.jwt); await signIn(page, host); await page.goto('/host'); // Wait for the dashboard to actually be loaded before asserting on an absence. await expect(page.getByRole('heading', { name: 'Host-Dashboard' })).toBeVisible({ timeout: 15_000, }); await expect(page.getByTestId('low-disk-warning')).toHaveCount(0); }); });