Atomicity: browser/backend/frontend/scraper recreated in one
docker compose up invocation instead of two sequential calls.
Graceful drain: before recreating scraper, poll scrape_runs for
running=0 up to 5min; WARNING+proceed on timeout (SIGTERM-drain
+ stop_grace_period + startup-reap below remain the safety net).
Startup-reap: mark orphaned running rows (heartbeat before a
DB-clock checkpoint taken right before stop) as cancelled instead
of waiting for the 6h zombie reaper. Implemented entirely in the
deploy workflow shell, not scheduler_main.py/scrapers, to avoid
touching files owned by the in-flight scraper_kit migration (#2277).
Review fixups: tr -d '[:space:]' was stripping the internal space
in the DB-clock timestamp literal (CAST(...timestamptz) failed
silently on every deploy) — switched to sed trim of leading/
trailing whitespace only. Drain-loop no longer conflates a failed
psql read with a confirmed running_count=0 (would have silently
skipped the drain wait on any transient docker-exec/DB hiccup).