Phase 17: run the migration, and fix the guard that locked out its own backup

Claire's writing — 8 documents, 33 snapshots, 103 suggestions, 3 vocabulary
words and an image — now belongs to her account rather than to the pre-auth
'local' user, and the VPS is canonical. millenia was left running and
untouched as a frozen fallback; it diverges the moment either side is
written to, so it wants retiring rather than syncing.

The plan's stated prerequisite, that she log in once so her subject exists,
turned out to be false. Authentik's hashed_user_id sub is the user's uid,
derived from her id and the instance secret, so it can be read in advance —
which means the data moves first and she signs in to find her writing
already there, instead of to an empty Petal that fills in later.

The fix here is to the liveness guard, and it is the second attempt at it.
PRAGMA locking_mode = EXCLUSIVE goes on holding its lock after being set
back to NORMAL — SQLite only lets go on that connection's next database
access — so against a real WAL database the script locked itself out of its
own VACUUM INTO backup. It passed locally because the test database had
come out of VACUUM INTO and so was never in WAL mode: the fixture didn't
look like production, the same way the stub identity provider's slashless
issuer didn't. The probe now runs on its own connection and closes it.

Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
This commit is contained in:
prosolis
2026-07-27 07:52:16 -07:00
parent 151df4565b
commit 84ee6bfb9c
3 changed files with 61 additions and 14 deletions
+43 -5
View File
@@ -4,12 +4,16 @@ Two deployments exist right now:
| | host | shape | status |
|---|---|---|---|
| **millenia** | `192.168.1.212` / `100.64.0.2` | bare binary in a screen session on `:8088`, Piper as user systemd units | **canonical** — her real writing lives here |
| **parodia** | `petal.parodia.dev` / `100.64.0.1` | docker compose behind the host's Traefik | staging; empty database |
| **parodia** | `petal.parodia.dev` / `100.64.0.1` | docker compose behind the host's Traefik | **canonical** since 2026-07-27she signs in here |
| **millenia** | `192.168.1.212` / `100.64.0.2` | `petal.service`, bare binary on `:8088`, Piper as user systemd units | frozen fallback: a copy of her writing as it stood at the move, still owned by the pre-auth `local` user |
millenia stays canonical until Phase 16's auth lands, so she only moves accounts
once. Everything below is the VPS side; the millenia Piper notes are kept in the
appendix because that instance still runs them.
Her writing moved to the VPS when sign-in landed (Phase 16/17): it is the
instance that actually authenticates, its data directory is LUKS-encrypted, and
it is reachable from anywhere. millenia was left running and untouched as a
fallback — but the two diverge the moment anything is written on either, so it
should be retired rather than kept in step. Everything below is the VPS side;
the millenia Piper notes are kept in the appendix because that instance still
runs them.
---
@@ -239,6 +243,40 @@ with `htpasswd -nbB petal 'your-password'` in `.env` as `PETAL_BASIC_AUTH`.
---
## 4a. Moving an account (`scripts/migrate_local_user.py`)
Petal ran as one hardcoded user (`users.id = 'local'`) before sign-in existed.
Moving that writing onto a real account is a deliberate, one-off operation:
```bash
docker compose stop petal
python3 scripts/migrate_local_user.py data/petal.db --to <oidc-sub> # dry run
python3 scripts/migrate_local_user.py data/petal.db --to <oidc-sub> \
--email her@example.com --name "Her Name" --apply
docker compose up -d petal
```
The subject id is **knowable before she has ever logged in**. With authentik's
default `hashed_user_id` sub mode it is the user's `uid`:
```bash
docker exec authentik-server-1 ak shell -c \
"from authentik.core.models import User; print(User.objects.get(username='claire').uid)"
```
so the data can move first and she signs in to find it already there.
**Start Petal once on the incoming database before migrating.** A database
carried over from another instance may be a schema behind, and the app applies
migrations at startup; the script moves rows and does not touch the schema.
The script is dry-run by default, takes its own `VACUUM INTO` backup, runs as
one transaction with foreign keys off, and verifies the row counts before it
commits. It refuses to run while anything else has the database open, and
refuses to merge into an account that already owns writing.
---
## 5. Backups
### On the VPS — folded into `parodia-backup`