Running `python scripts/export_catalog.py` on a deployed site died with
1045 "Access denied for user 'root'@'localhost'", naming a user that site
does not have.
Nothing loads .env for a plain script run. wsgi.py calls load_dotenv() for the
served app and the flask CLI loads .env for itself, so both reach the real
database; a bare `python scripts/...` gets neither. DATABASE_URL then stays
unset, FLASK_ENV defaults to development, and config.py falls back to a
hardcoded root@localhost URL.
The read-only failure above is the harmless version. Six of these seven
scripts WRITE - backfill, reclassify, retype, collapse, refile, import - and
on any box where those fallback root credentials happen to resolve, they would
have silently rewritten the wrong database with no error at all. That is also
why this survived: the dev .env points at root, so the fallback matched what
the developer expected and nothing ever looked wrong here.
All seven now load .env from the repo root, resolved from __file__ rather than
the working directory, since these are run by hand during a deployment and
rarely from the repo root.
tests/test_maintenance_scripts.py pins it for any future script: every script
that calls create_app must call load_dotenv, before create_app rather than
after it, and with an explicit path rather than a cwd-relative search.
Backups collected before markers became assets were filed against the operation
number the PC reported. The collector now files a marker PC's backup against
its marker, but only for backups collected since; everything already stored
stayed where it was, so the new marker assets read as empty while the operation
holds a mixed history.
The attribution is exact rather than a guess. A revision records the PC it was
read from, and the collector records which marker each PC drives, so the move
is that mapping applied. Revisions with no source hostname stay put, since
nothing says which marker they came from.
Moving a revision can duplicate one the collector has already written against
the marker, so the script collapses those as part of the move, keeping the
earliest so the date a config was first seen survives. Dry run by default.