fix(e12): close what round one broke, and revert two fixes that overreached

Round two of the review loop, on round one's own diff. Two of its findings were
regressions the fixes themselves introduced, which is exactly what the loop is for:

**A per-user refusal was writing onto a shared row.** MediaAssets are deduped across
users, and the generic failure handler puts the message on the asset — right for an
upstream failure, wrong for "your storage is full". Two users queuing the same video share
one asset, so the first user hitting their cap failed the second user's job with the first
one's private reason, telling someone with an empty library to free up space. The refusal
now raises `_OwnerRefused`, caught separately: the job fails, the asset goes back to
`pending` for whoever else holds it. Pinned by a test that drives `_download` directly —
the refusal happens before extraction, so nothing needs stubbing.

**The restore scripts restarted the app, undoing the restore.** `docker compose start`
re-runs the container command, and that runs `alembic upgrade head` — so on the rollback
path this documents as recommended (restore the dump, THEN pin the older tag) the newer
image immediately re-applied the migrations the restore had just undone, and on a FAILED
restore it put the app back on a half-restored schema. Both are the opposite of why the
stop was added. Nothing is restarted automatically now; the scripts say what to run and
when, and the docs' snippets say the same.

**Reverted rather than patched:** the OnboardingWizard toast (the wizard already renders
that failure inline, next to a Retry button — three recoveries for one failure), and
`useVerifyEntry`'s "each new link clears the other". The latter blanked the signed-out
card's only banner, where nothing renders the reset flag to replace it. The two states are
independent again; what changes is that `dismiss` peels ONE layer, which is what the
original finding was actually about.

Also: `Stats` uses the YouTube helper rather than the bare one, so the route's English 403
detail no longer beats the translated fallback under a Hungarian UI; `urlparse` guarded
alongside `.port` (an unclosed bracket raised just as late), the reason renamed `malformed`
since "bad port" misdescribes it; three docstrings narrowed to what the code actually does
(the worker's byte check is per-job, not an aggregate ledger, and not on the edit path;
the redirect residual leaks a reachability oracle, which the note had denied; the
invalidation helper does not cover suspend); the pasted Windows recipe wrapped so `throw`
can stop it; the bash one-liner's failure branch no longer exits 0; and the tests that were
missing for the round-one fixes — the account panel's error paths, the invalidation set,
and the shared-asset case above.

Gate green: vitest 685, pytest 950.
This commit is contained in:
2026-09-05 04:02:57 +02:00
parent fe38dfbcca
commit 358d722ce9
25 changed files with 443 additions and 125 deletions
+25 -9
View File
@@ -183,7 +183,7 @@ mkdir -p backups && out="backups/siftlode-$(date +%Y%m%d-%H%M%S).dump" && \
docker compose -f docker-compose.selfhost.yml exec -T db \
pg_dump -U siftlode -Fc siftlode > "$out.part" \
&& [ -s "$out.part" ] && mv "$out.part" "$out" && echo "wrote $out" \
|| { rm -f "$out.part"; echo "BACKUP FAILED"; }
|| { rm -f "$out.part"; echo "BACKUP FAILED" >&2; false; }
# Restore. Stop the app first: pg_restore --clean DROPs each object in its own transaction, and a
# running api holds locks that make those DROPs queue — abort the wait and you are left with a
@@ -192,7 +192,11 @@ mkdir -p backups && out="backups/siftlode-$(date +%Y%m%d-%H%M%S).dump" && \
docker compose -f docker-compose.selfhost.yml stop api worker
docker compose -f docker-compose.selfhost.yml exec -T db \
pg_restore -U siftlode -d siftlode --clean --if-exists < backups/siftlode-<timestamp>.dump
docker compose -f docker-compose.selfhost.yml up -d
# Start it again only once the restore reported no errors — and, if you are rolling a release back,
# only AFTER pinning the older IMAGE_TAG. Every start runs `alembic upgrade head`, so starting the
# newer image here would re-apply the migrations you just restored away.
docker compose -f docker-compose.selfhost.yml start api worker
```
On Windows, do **not** pipe the dump through PowerShell — it corrupts binary data. Write it inside the
@@ -200,13 +204,24 @@ container and copy it out instead. Check the exit code between the two steps and
name: the dump inside the container keeps its name, so a `cp` that runs after a *failed* dump would
copy out the previous run's file and quietly overwrite your only backup with a stale one.
Paste it as ONE block. `throw` only aborts the statement it is in, so as separate pasted lines the
copy would still run after a failed dump — and copy out the previous run's file:
```powershell
New-Item -ItemType Directory -Force -Path backups | Out-Null
$out = "backups\siftlode-$(Get-Date -Format yyyyMMdd-HHmmss).dump"
docker compose -f docker-compose.selfhost.yml exec -T db pg_dump -U siftlode -Fc siftlode -f /tmp/s.dump
if ($LASTEXITCODE -ne 0) { throw "BACKUP FAILED - pg_dump returned $LASTEXITCODE" }
docker compose -f docker-compose.selfhost.yml cp db:/tmp/s.dump $out
docker compose -f docker-compose.selfhost.yml exec -T db rm -f /tmp/s.dump
& {
$compose = "docker-compose.selfhost.yml"
New-Item -ItemType Directory -Force -Path backups | Out-Null
$out = "backups\siftlode-$(Get-Date -Format yyyyMMdd-HHmmss).dump"
docker compose -f $compose exec -T db pg_dump -U siftlode -Fc siftlode -f /tmp/s.dump
$code = $LASTEXITCODE
if ($code -ne 0) { throw "BACKUP FAILED - pg_dump returned $code" }
docker compose -f $compose cp db:/tmp/s.dump $out
$code = $LASTEXITCODE
docker compose -f $compose exec -T db rm -f /tmp/s.dump
if ($code -ne 0) { throw "BACKUP FAILED - could not copy the dump out (exit $code)" }
if ((Get-Item $out).Length -eq 0) { Remove-Item $out; throw "BACKUP FAILED - the dump came out empty" }
"wrote $out"
}
```
## Upgrading
@@ -250,7 +265,8 @@ exactly where the old image expects it, in one step, with no revision arithmetic
```bash
# 1. stop the app (leave the database up)
docker compose -f docker-compose.selfhost.yml stop api worker
# 2. restore the dump you took before upgrading (see Backups above)
# 2. restore the dump you took before upgrading (see Backups above). Leave the app stopped:
# starting it here would run the NEWER image's migrations against the database you just restored.
docker compose -f docker-compose.selfhost.yml exec -T db \
pg_restore -U siftlode -d siftlode --clean --if-exists < backups/siftlode-<timestamp>.dump
# 3. pin the previous version in .env, then start again