Prod env files now win over root .env so harmonize:db can open gallery_prod after gallery_dev; tour_stops gains updated_at; halls no longer block the overlay on every painting texture. Docs cover the fixes and one-artist image pull from prod; Duccio painting assets synced from TrueNAS. Co-authored-by: Cursor <cursoragent@cursor.com>
189 lines
7.3 KiB
Markdown
189 lines
7.3 KiB
Markdown
# Harmonize dev and prod (incremental merge)
|
||
|
||
Bidirectional **catalog data** and **image** sync between `gallery_dev` and `gallery_prod`, with **last-write-wins** by timestamp. Use this when **both** environments may have curator edits since the last release — not when prod should become an exact copy of dev.
|
||
|
||
For a full prod replace (weekly release), use [deploy-dev-to-prod.md](deploy-dev-to-prod.md) (`npm run devtoprod:release`).
|
||
|
||
For refreshing dev from prod entirely, use `npm run prodto:dev:db` (destructive to dev).
|
||
|
||
---
|
||
|
||
## Rules
|
||
|
||
| Layer | Direction | Conflict resolution |
|
||
|-------|-----------|---------------------|
|
||
| **Schema** | dev → prod only | Run `npm run harmonize:schema` (same as `dev:migrate` on `gallery_prod`) |
|
||
| **Catalog DB** | dev ↔ prod | Newer `updated_at` wins; missing rows copied to the other side (union merge) |
|
||
| **Images** | dev ↔ prod | Newer file mtime wins; missing files copied to the other side |
|
||
| **Users / sessions / audit** | not synced | `users`, `session`, `curator_audit_log` stay env-local |
|
||
|
||
**Never auto-deletes** rows or files that exist on only one side.
|
||
|
||
---
|
||
|
||
## Prerequisites
|
||
|
||
1. Both databases on Postgres `192.168.10.122` with `updated_at` columns applied:
|
||
|
||
```powershell
|
||
npm run dev:migrate
|
||
$env:DB_NAME = "gallery_prod"; npm run dev:migrate; Remove-Item Env:\DB_NAME
|
||
```
|
||
|
||
2. Backfill `updated_at` from image file mtimes (recommended once after migration):
|
||
|
||
```powershell
|
||
npm run dev:backfill-updated-at
|
||
npm run harmonize:backfill-updated-at
|
||
```
|
||
|
||
3. [`infra/docker/.env.prod`](../infra/docker/.env.prod) present with `DB_NAME=gallery_prod`.
|
||
|
||
4. SMB share reachable for prod images:
|
||
|
||
```powershell
|
||
net use \\192.168.10.122\Gallery /user:YOUR_TRUENAS_USER
|
||
```
|
||
|
||
5. Copy **dedicated** harmonize config (do not rely on `devtoprod.config.json` alone — that file is a release profile and its step flags are ignored for harmonize):
|
||
|
||
```powershell
|
||
Copy-Item infra/deploy/harmonize.config.example.json infra/deploy/harmonize.config.json
|
||
```
|
||
|
||
Edit `harmonize.config.json` (gitignored) — optional `smb.user` / `smb.password`, `prefer` for tie-breaks (`dev` | `prod`), `schemaChanged: true` when new migrations shipped.
|
||
|
||
6. Prod DB targeting uses [`infra/docker/.env.prod`](../infra/docker/.env.prod). `scripts/db-env.js` loads that file with **file values winning** over the root `.env`, so `harmonize:db` can open `gallery_dev` and `gallery_prod` in the same process without the safety check rejecting `gallery_dev` as a fake prod target.
|
||
|
||
---
|
||
|
||
## One-command harmonize
|
||
|
||
```powershell
|
||
npm run harmonize
|
||
```
|
||
|
||
Dry-run (report only, no writes):
|
||
|
||
```powershell
|
||
npm run harmonize -- -DryRun
|
||
```
|
||
|
||
Or:
|
||
|
||
```powershell
|
||
npm run harmonize:dry-run
|
||
```
|
||
|
||
### Orchestrator steps
|
||
|
||
| Step | npm script | Purpose |
|
||
|------|------------|---------|
|
||
| `backupDev` | `dev:db:backup` | Safety snapshot |
|
||
| `backupProd` | `prod:db:backup` | Safety snapshot |
|
||
| `schema` | `harmonize:schema` | Apply dev migrations to prod (when `schemaChanged: true`) |
|
||
| `db` | `harmonize:db` | Row-level catalog merge |
|
||
| `images` | `harmonize:images` | Bidirectional file merge |
|
||
| `verify` | curl `/api/bounds` | Optional smoke check |
|
||
|
||
Reports are written to `db/SyncReports/harmonize_db_*.json` and `harmonize_images_*.json` (gitignored).
|
||
|
||
---
|
||
|
||
## Individual commands
|
||
|
||
| Command | Purpose |
|
||
|---------|---------|
|
||
| `npm run harmonize:schema` | Migrate prod schema from dev migration files |
|
||
| `npm run harmonize:db` | Merge catalog rows by `updated_at` |
|
||
| `npm run harmonize:db -- --dry-run` | Preview DB changes |
|
||
| `npm run harmonize:db -- --prefer=dev` | On equal `updated_at`, dev wins |
|
||
| `npm run harmonize:images` | Merge image files by mtime |
|
||
| `npm run dev:migrate:sync-timestamps` | Apply `updated_at` migration on dev only |
|
||
| `npm run dev:backfill-updated-at` | Backfill dev `updated_at` from image mtimes |
|
||
| `npm run harmonize:backfill-updated-at` | Same backfill on prod |
|
||
|
||
---
|
||
|
||
## Catalog tables synced
|
||
|
||
Processed in FK order:
|
||
|
||
`historical_eras` → `art_movements` → `artists` → `artist_periods` → `paintings` → `painting_influences` → `painting_influence_sources` → `painting_annotations` → **`entity_translations`** → **`tours`** → **`tour_stops`**
|
||
|
||
Every synced table needs an `updated_at` column (including `tour_stops` — added via `migrate-tours.sql` / `migrate-sync-timestamps.sql`).
|
||
|
||
---
|
||
|
||
## Conflict handling
|
||
|
||
Harmonize reports conflicts in the JSON report and skips those rows:
|
||
|
||
| Conflict | Cause | Resolution |
|
||
|----------|-------|------------|
|
||
| `id_collision` | Same `id` but different natural key (e.g. artist name) | Manual fix in pgAdmin; environments diverged too far |
|
||
| `equal_updated_at` | Same timestamp, different row content | Re-run with `--prefer=dev` or `--prefer=prod`, or edit one side and re-run |
|
||
|
||
**Tip:** Harmonize regularly from a shared baseline (e.g. after each weekly release) to avoid ID/natural-key collisions from independent inserts.
|
||
|
||
---
|
||
|
||
## When to use what
|
||
|
||
| Situation | Tool |
|
||
|-----------|------|
|
||
| Weekly release — prod should match dev exactly | `npm run devtoprod:release` |
|
||
| Mid-week prod curator fix + dev also changed | `npm run harmonize` |
|
||
| Dev workspace stale — full prod copy | `npm run prodto:dev:db` |
|
||
| New migration in repo | `harmonize:schema` or deploy step 4 |
|
||
| Only images changed on one side | `npm run harmonize:images` |
|
||
| Only DB metadata changed | `npm run harmonize:db` |
|
||
| Pull **one artist’s** images from prod → dev | Map SMB, then robocopy `Duccio*` (or the artist prefix) under `paintings/` and `paintings/thumbs/` — see [FAC.md — Dev ↔ prod image sync](FAC.md#dev--prod-image-sync-smb) |
|
||
|
||
---
|
||
|
||
## Example flows
|
||
|
||
### Prod curator uploaded a painting; dev also edited metadata
|
||
|
||
```powershell
|
||
npm run harmonize
|
||
```
|
||
|
||
DB rows merge by `updated_at`; image files merge by mtime. Both sides receive the latest version of each entity.
|
||
|
||
### Schema change + mixed edits
|
||
|
||
1. Finish and test on dev: `npm run dev:migrate`
|
||
2. Set `schemaChanged: true` in `harmonize.config.json` (or enable `steps.schema`)
|
||
3. `npm run harmonize`
|
||
|
||
### Preview before writing
|
||
|
||
```powershell
|
||
npm run harmonize:dry-run
|
||
# Review db/SyncReports/harmonize_*.json
|
||
npm run harmonize
|
||
```
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
| Symptom | Likely cause | Fix |
|
||
|---------|--------------|-----|
|
||
| `Refusing prod operation on database "gallery_dev"` | Old `db-env` left `DB_NAME` from root `.env` when loading prod | Update to current `scripts/db-env.js` (`.env.prod` wins); confirm `infra/docker/.env.prod` has `DB_NAME=gallery_prod` |
|
||
| `Table tour_stops missing updated_at` | Prod/dev schema behind | `npm run dev:migrate` and migrate prod (`$env:DB_NAME="gallery_prod"; npm run dev:migrate; Remove-Item Env:\DB_NAME`) |
|
||
| Orchestrator lists release steps (`restoreProd`, …) | Missing `harmonize.config.json`; fallback to release config | Copy `harmonize.config.example.json` → `harmonize.config.json` (current `harmonize.ps1` ignores release-only step keys) |
|
||
|
||
---
|
||
|
||
## Safety
|
||
|
||
- Pre-flight backups of dev and prod DB (configurable; on by default)
|
||
- Prod writes require `yes` or `CONFIRM_PROD=1` (orchestrator sets `CONFIRM_PROD=1` when `autoConfirm`-style run)
|
||
- No TRUNCATE — harmonize only inserts/updates changed rows
|
||
- Rollback: restore from `db/DataBackup/gallery_*_data_*.txt` using `dev:db:restore` or `devtoprod:db:restore`
|
||
|
||
See also [environments.md](environments.md) and [deploy-dev-to-prod.md](deploy-dev-to-prod.md).
|