fix(e12): close the eighteen findings the E12 review round left standing
The deep review of the branch raised 47 findings, 18 of which survived adversarial verification. Grouped by what they actually were: **The size ceiling inverted at its boundary.** `remaining_bytes` clamped to 0 for a user already at their storage cap, and `build_ydl_opts` reads 0 as "no limit" — so the one user over quota was the only one downloading with no ceiling at all. `check_enqueue` could not catch it: it ran while the job was queued and the user was still under the cap. The arithmetic moves into `quota.remaining_bytes` (where every other max_bytes decision lives, and where it can be tested), and the worker now fails the job outright rather than proceeding unbounded — as a non-retryable `quota_exceeded`, since retrying cannot help until space is freed. **Silent failures in the new account panel.** All three actions went through `quiet: true` endpoints, which suppress the global error dialog on the promise that the caller reports inline — with no `onError`, that promise was broken and a refusal rendered as nothing. On the broken-SMTP instance this panel exists for, an admin would read silence as "the mail went out". The co-invalidation set and the email-off predicate move to `lib/adminUsers`, shared with the pending-access queue, so the two copies of these mutations cannot drift apart again — which is how half of this was lost in the first place. **A rollback recipe that destroyed data without working.** `alembic downgrade -1` steps back one migration, but a release routinely ships several (0.80.0 shipped four): it drops the newest one's columns and leaves the schema at a revision the older image also cannot resolve, i.e. the crash loop it was meant to escape, minus the data. The dump restore is now the recommended path, and the alembic route asks the target version for its own head revision instead of counting backwards. Also: redirects named in the urlguard docstring beside the DNS-rebinding residual they share a fix with (SEC-17); a malformed port refused as `bad_port` instead of escaping as a 500; the pull cooldown extended to the sibling route spending the same QuotaAction; restore stops the app before dropping its tables; the PowerShell cleanup can no longer decide the script's verdict on 5.1, nor report the wrong exit code; the documented one-liners hardened to match the scripts; the registry's retention window stated where pinning is advertised; the two banners made mutually exclusive so neither can shadow the other; `hover:bg-hover` (a utility this project never generated) replaced by the `selectable` recipe; the clipboard helper reused; and the sync cooldown's copy swept to the two callers that discarded it. Verified: gate green (vitest 678, pytest 949), and the panel, the row hover and the reset banner UAT'd on localdev.
This commit is contained in:
+17
-4
@@ -12,6 +12,15 @@ $ErrorActionPreference = "Stop"
|
||||
# encoding name", so this script simply failed there.
|
||||
$remote = "/tmp/siftlode-backup.dump"
|
||||
|
||||
# Best-effort cleanup of the in-container dump. Deliberately NOT `docker compose ... 2>$null`: on
|
||||
# Windows PowerShell 5.1 a redirected native stderr becomes an ErrorRecord, and the
|
||||
# $ErrorActionPreference above makes it TERMINATING — so one stray Compose warning would kill the
|
||||
# script here, masking the real diagnostic below or reporting a finished backup as a failure. A
|
||||
# cleanup step must never decide the verdict.
|
||||
function Remove-RemoteDump($path) {
|
||||
try { docker compose exec -T db rm -f $path 2>&1 | Out-Null } catch { }
|
||||
}
|
||||
|
||||
# `docker compose` finds docker-compose.yml on its own and honours $env:COMPOSE_FILE. The documented
|
||||
# self-host layout (docs/self-hosting.md § Get the files) has NEITHER — just the selfhost file.
|
||||
if (-not $env:COMPOSE_FILE -and -not (Test-Path "docker-compose.yml") -and (Test-Path "docker-compose.selfhost.yml")) {
|
||||
@@ -25,14 +34,18 @@ $ts = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$out = "backups\siftlode-$ts.dump"
|
||||
|
||||
docker compose exec -T db pg_dump -U $user -Fc $dbname -f $remote
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
docker compose exec -T db rm -f $remote 2>$null
|
||||
throw "BACKUP FAILED — pg_dump returned $LASTEXITCODE; no file was written."
|
||||
# Capture BEFORE the cleanup call: any native command overwrites $LASTEXITCODE, so interpolating it
|
||||
# after Remove-RemoteDump would report the cleanup's code (0) inside a branch that only runs when
|
||||
# pg_dump failed — a message contradicting itself. Same reason $cpCode is captured below.
|
||||
$dumpCode = $LASTEXITCODE
|
||||
if ($dumpCode -ne 0) {
|
||||
Remove-RemoteDump $remote
|
||||
throw "BACKUP FAILED — pg_dump returned $dumpCode; no file was written."
|
||||
}
|
||||
|
||||
docker compose cp "db:$remote" $out
|
||||
$cpCode = $LASTEXITCODE
|
||||
docker compose exec -T db rm -f $remote 2>$null
|
||||
Remove-RemoteDump $remote
|
||||
if ($cpCode -ne 0) { throw "BACKUP FAILED — could not copy the dump out of the container (exit $cpCode)." }
|
||||
if (-not (Test-Path $out) -or (Get-Item $out).Length -eq 0) {
|
||||
if (Test-Path $out) { Remove-Item $out }
|
||||
|
||||
+27
-10
@@ -11,6 +11,13 @@ $ErrorActionPreference = "Stop"
|
||||
# which corrupts binary on 5.1 and cannot express `-Encoding Byte` at all on 7.
|
||||
$remote = "/tmp/siftlode-restore.dump"
|
||||
|
||||
# See backup.ps1: `2>$null` on a native command is a TERMINATING error under $ErrorActionPreference
|
||||
# = "Stop" on Windows PowerShell 5.1, so a cleanup step could kill the script after the restore
|
||||
# already succeeded — and the obvious next move, re-running the restore, wipes the database again.
|
||||
function Remove-RemoteDump($path) {
|
||||
try { docker compose exec -T db rm -f $path 2>&1 | Out-Null } catch { }
|
||||
}
|
||||
|
||||
if (-not $env:COMPOSE_FILE -and -not (Test-Path "docker-compose.yml") -and (Test-Path "docker-compose.selfhost.yml")) {
|
||||
$env:COMPOSE_FILE = "docker-compose.selfhost.yml"
|
||||
}
|
||||
@@ -20,16 +27,26 @@ $dbname = if ($env:POSTGRES_DB) { $env:POSTGRES_DB } else { "siftlode" }
|
||||
if (-not (Test-Path $DumpFile)) { throw "Dump file not found: $DumpFile" }
|
||||
|
||||
docker compose cp $DumpFile "db:$remote"
|
||||
if ($LASTEXITCODE -ne 0) { throw "RESTORE FAILED — could not copy the dump into the container (exit $LASTEXITCODE)." }
|
||||
$cpCode = $LASTEXITCODE
|
||||
if ($cpCode -ne 0) { throw "RESTORE FAILED — could not copy the dump into the container (exit $cpCode)." }
|
||||
|
||||
docker compose exec -T db pg_restore -U $user -d $dbname --clean --if-exists $remote
|
||||
$restoreCode = $LASTEXITCODE
|
||||
docker compose exec -T db rm -f $remote 2>$null
|
||||
# Stop the app before dropping its tables. `--clean` DROPs each object in its own transaction, and a
|
||||
# running api (with its scheduler) holds locks that make those DROPs queue behind it — abort the
|
||||
# wait and the database is left half-dropped. It also keeps the app from writing into a schema that
|
||||
# is only partly back. Restarted in `finally`, so a failed restore does not leave the stack down.
|
||||
docker compose stop api worker
|
||||
try {
|
||||
docker compose exec -T db pg_restore -U $user -d $dbname --clean --if-exists $remote
|
||||
$restoreCode = $LASTEXITCODE
|
||||
Remove-RemoteDump $remote
|
||||
|
||||
# pg_restore exits non-zero for ignorable warnings too (--clean --if-exists on a fresh database
|
||||
# reports every missing object it tried to drop), so a non-zero code is reported as "check this",
|
||||
# not swallowed and not dressed up as success.
|
||||
if ($restoreCode -ne 0) {
|
||||
throw "pg_restore reported errors while restoring $DumpFile (exit $restoreCode) — read the output above before treating this instance as restored."
|
||||
# pg_restore exits non-zero for ignorable warnings too (--clean --if-exists on a fresh database
|
||||
# reports every missing object it tried to drop), so a non-zero code is reported as "check this",
|
||||
# not swallowed and not dressed up as success.
|
||||
if ($restoreCode -ne 0) {
|
||||
throw "pg_restore reported errors while restoring $DumpFile (exit $restoreCode) — read the output above before treating this instance as restored."
|
||||
}
|
||||
Write-Output "Restored $DumpFile"
|
||||
} finally {
|
||||
docker compose start api worker | Out-Null
|
||||
}
|
||||
Write-Output "Restored $DumpFile"
|
||||
|
||||
@@ -20,6 +20,15 @@ if [ ! -f "$FILE" ]; then echo "Dump file not found: $FILE"; exit 1; fi
|
||||
USER_NAME="${POSTGRES_USER:-siftlode}"
|
||||
DB_NAME="${POSTGRES_DB:-siftlode}"
|
||||
|
||||
# Stop the app before dropping its tables. `--clean` DROPs each object in its own transaction, and a
|
||||
# running api (with its scheduler) holds locks that make those DROPs queue behind it — abort the wait
|
||||
# and the database is left half-dropped. It also keeps the app from writing into a schema that is
|
||||
# only partly back. The trap restarts it however this script ends, so a failed restore does not leave
|
||||
# the stack down.
|
||||
docker compose stop api worker
|
||||
restart_app() { docker compose start api worker >/dev/null 2>&1 || true; }
|
||||
trap restart_app EXIT
|
||||
|
||||
# pg_restore exits non-zero for ignorable warnings too (--clean --if-exists on a fresh database
|
||||
# reports every missing object it tried to drop), so a non-zero code is reported as "check this",
|
||||
# not swallowed and not dressed up as success.
|
||||
|
||||
Reference in New Issue
Block a user