diff --git a/README.md b/README.md index b8f5414..6eceddf 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,88 @@ or `starter`. Admin UI: `/admin/dev-deploy/` (logged in as an admin). Machine API: `/dev-deploy/api/{journal,ingest,file/:uuid,health}` (HMAC-signed peer requests). +### Pairing two instances + +Do this once per pair (e.g. MAIN ↔ TEST). Each side needs the other's +`env_id` (a UUID shown on its `/admin/dev-deploy/` dashboard) and base URL. + +1. **On instance A** — go to `/admin/dev-deploy/peers`. Under "Add peer", fill + in B's `env_id`, a label, B's base URL, and leave "Existing secret" blank. + Click **Pair**. A's response shows a 64-hex shared secret **once** — copy it. +2. **On instance B** — `/admin/dev-deploy/peers` → "Add peer". Fill in A's + `env_id` + base URL, paste the secret from step 1 into "Existing secret", + click **Pair**. + +Both sides now have a peer row with a sealed copy of the shared secret. The +plain secret is never persisted on either side — only the AES-256-GCM +ciphertext (KEK derived from `SALTCORN_SESSION_SECRET`). Use **Rotate** on +either side to roll the secret (the peer page surfaces the new one once; +paste it on the other side via Rotate). + +### Promoting changes (source → target) + +1. Make metadata edits on the source (e.g. create a table on MAIN). +2. Go to `/admin/dev-deploy/plan`, pick the target peer, click **Show plan**. + Lists the ops that would be sent (everything since the last outbound + anchor for that peer). +3. Click **Promote N ops**. The target applies them, advances the outbound + anchor, and returns per-op results (applied / error / conflict counts in + the redirect flash). + +Plugin-list mismatches between source and target are surfaced as warnings in +the same flash message (e.g. "peer missing plugin X", "plugin version mismatch +on Y"). They don't block the promote — they're advisory. + +### Pulling changes (target ← source) + +On the target, `/admin/dev-deploy/peers` → **Pull** on the source's peer row. +Fetches everything since the last inbound anchor and applies it. If any op +conflicts with a local op on the same entity since the last sync, the incoming +op is journaled with `status='conflict'` and **not** applied; the flash +redirects to `/admin/dev-deploy/conflicts`. + +### Resolving conflicts + +A conflict means both sides edited the same entity since the last sync. On +the target side at `/admin/dev-deploy/conflicts`, each pending conflict shows +the incoming op (theirs) next to the local op (mine): + +- **Use theirs** — apply the incoming op now, overwriting the local change. + The local op stays in the journal but its effect is gone. +- **Use mine** — mark the incoming op `rejected`. Local state stands. The + peer keeps trying to ship the op on future pulls; we skip it by op_id. +- **Merge per field** (only for `update_X` vs `update_X` conflicts) — opens + a form with each diverging field. Pick **keep current**, **take incoming**, + or type a custom value per field. Submitting marks the conflict `merged`. + +### Per-table data mode + +By default, table **rows** are local-only (`user` mode) and dev-deploy only +migrates the table's schema. Change this per-table at `/admin/dev-deploy/tables`: + +- **user** — rows belong to each environment; never synced. The Saltcorn + `users` table is hard-locked to this mode. +- **starter** — on the first promote, ships current rows once. The target + then owns them; future row changes on the source don't propagate. +- **managed** — rows always sync from source. Source is canonical; target's + row edits get overwritten or surface as row conflicts. + +Switching to `managed` or `starter` adds a hidden `_dd_row_uuid` column to +the table via raw `ALTER` (not registered in `_sc_fields`, so Saltcorn's +table builder doesn't show it) and ships current rows in the next promote. +Switching back to `user` drops the column (best-effort; older SQLite without +DROP COLUMN support will error). FKs to user-mode tables are NULL'd on the +target with a warning attached to the op. + +### Reverting an op + +The journal at `/admin/dev-deploy/ops` shows every op with a **Revert** button. +Revert appends a compensating op (`create_X` → drop; `drop_X` → recreate from +the captured before-snapshot; `update_X` → re-apply before-snapshot as a +patch) rather than rewriting history. The inverse op promotes to peers like +any other op on the next push. Reverting a `drop` produces a new entity with +a fresh UUID — content is restored, identity is not. + ## Tests ```bash