chore(deploy): expose the export rebuild hatch in the UI; rehearse migration 014
Three deploy-readiness gaps, none of them in the export invariant itself. 1. The escape hatch was unreachable. `POST /host/export/rebuild` was added as the fix for "a failed export is terminal", but nothing in the frontend called it — so recovery required a terminal, the API docs and a valid JWT. The host page instead told the host to reopen uploads and re-release, which is the destructive act the endpoint exists to avoid (it unlocks the gallery to every guest and retracts the release). The failed state now offers "Erneut versuchen"; a ready keepsake can be rebuilt behind a confirm, since a rebuild makes it briefly undownloadable. 2. Migration 014 had never been run against anything. It is the repo's first destructive migration and its backfill has to carry the old notion of "downloadable" across exactly — get it wrong and a released event's keepsake silently 404s, on data that cannot be re-created. backend/scripts/rehearse-014.sh proves it on a throwaway postgres, against a real pg_dump or a synthetic DB covering every pre-014 state, asserting up, down and up-again all preserve downloadability. Verified non-vacuous: retiring nothing (ELSE -1 -> ELSE e.export_epoch) fails it, naming the three keepsakes it would resurrect. It also surfaced that the down migration is an inverse UP TO DRIFT: a ready flag that disagreed with its job row comes back FALSE. That heals corruption rather than restoring it, and costs nothing (no file_path to serve), so the assertion checks what actually matters — no genuinely downloadable keepsake loses its flag — and reports the normalisation instead of failing on it. 3. No advisory scanning. cargo audit + npm audit, in .github/workflows/ because Gitea Actions scans it too and e2e.yml already resolves actions/* from GitHub. The runner is not assumed to have cargo. RUSTSEC-2023-0071 (rsa/Marvin) is ignored SPECIFICALLY, not globally: it is unreachable here (HS256 + bcrypt, Postgres only) and unpatched, so failing on it would mean a permanently red pipeline everyone learns to ignore. Tests: e2e pins that a failed keepsake recovers via rebuild while the event stays released and uploads stay locked — so a future refactor implementing rebuild as an internal reopen+re-release fails — and that rebuild cannot publish an unreleased gallery. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,7 @@
|
||||
let users = $state<UserSummary[]>([]);
|
||||
let pinResetRequests = $state<PinResetRequest[]>([]);
|
||||
let exportInfo = $state<ExportStatusDto | null>(null);
|
||||
let rebuilding = $state(false);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
@@ -291,6 +292,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
// The escape hatch for a keepsake that is failed or stale. Without this the only recovery is
|
||||
// reopening uploads (which unlocks the gallery to every guest and retracts the release) or a
|
||||
// container restart — neither of which a host at 2am can reasonably be asked to do.
|
||||
async function rebuildExport() {
|
||||
rebuilding = true;
|
||||
try {
|
||||
await api.post('/host/export/rebuild', {});
|
||||
toast('Keepsake wird neu erstellt…', 'success');
|
||||
await refreshExportStatus();
|
||||
} catch (e: unknown) {
|
||||
toastError(e);
|
||||
} finally {
|
||||
rebuilding = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openBanModal(user: UserSummary) {
|
||||
banTarget = user;
|
||||
}
|
||||
@@ -598,10 +615,38 @@
|
||||
{:else if exportReady}
|
||||
<p class="flex items-center justify-between gap-2">
|
||||
<span class="font-medium text-green-700 dark:text-green-300">Keepsake ist bereit.</span>
|
||||
<a href="/export" class="font-medium text-blue-600 underline dark:text-blue-400">Zum Download</a>
|
||||
<span class="flex items-center gap-3">
|
||||
<!-- Rebuilding a READY keepsake is disruptive: guests who tap Download during the
|
||||
rebuild get nothing until it finishes. Worth a confirm, unlike the failed case. -->
|
||||
<button
|
||||
onclick={() => (confirmAction = {
|
||||
title: 'Keepsake neu erstellen?',
|
||||
message:
|
||||
'Das Keepsake wird aus dem aktuellen Stand der Galerie neu erzeugt. ' +
|
||||
'Während der Erstellung können Gäste es nicht herunterladen.',
|
||||
confirmLabel: 'Neu erstellen',
|
||||
tone: 'danger',
|
||||
run: rebuildExport
|
||||
})}
|
||||
disabled={rebuilding}
|
||||
data-testid="export-rebuild"
|
||||
class="font-medium text-gray-500 underline disabled:opacity-50 dark:text-gray-400"
|
||||
>
|
||||
Neu erstellen
|
||||
</button>
|
||||
<a href="/export" class="font-medium text-blue-600 underline dark:text-blue-400">Zum Download</a>
|
||||
</span>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-red-700 dark:text-red-300">Keepsake-Erstellung fehlgeschlagen. Öffne die Uploads erneut und gib die Galerie neu frei.</p>
|
||||
<p class="text-red-700 dark:text-red-300">Keepsake-Erstellung fehlgeschlagen.</p>
|
||||
<button
|
||||
onclick={rebuildExport}
|
||||
disabled={rebuilding}
|
||||
data-testid="export-rebuild"
|
||||
class="mt-2 rounded-lg bg-red-600 px-3 py-1.5 text-xs font-medium text-white transition hover:bg-red-700 disabled:opacity-50 dark:bg-red-500 dark:hover:bg-red-400"
|
||||
>
|
||||
{rebuilding ? 'Wird gestartet…' : 'Erneut versuchen'}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user