From 67594ea4d3e068e9f558f4859e8fdee49e0edba6 Mon Sep 17 00:00:00 2001 From: Danila Khodjaef Date: Mon, 27 Jul 2026 14:33:36 +0300 Subject: [PATCH] Sync checkup flags with image harmonize; tighten gallery collision and exit-to-timeline. Co-authored-by: Cursor --- Documentation/FAC.md | 2 +- Documentation/data-and-images.md | 4 +- Documentation/environments.md | 2 +- Documentation/harmonize-dev-prod.md | 9 +- client/src/components/VirtualGallery.css | 6 + client/src/components/VirtualGallery.tsx | 214 +++++++++++++++++++---- scripts/harmonize-images.js | 71 +++++++- 7 files changed, 260 insertions(+), 48 deletions(-) diff --git a/Documentation/FAC.md b/Documentation/FAC.md index 1fcd0f4..94fa6e1 100644 --- a/Documentation/FAC.md +++ b/Documentation/FAC.md @@ -143,7 +143,7 @@ Run `npm run dev:migrate` against prod DB after first deploy with auth vars set | `npm run devtoprod:db:restore -- --file ` | Restore into **prod** (requires confirmation) | | `npm run harmonize` | Bidirectional catalog DB + image merge by `updated_at` / file mtime — [harmonize-dev-prod.md](harmonize-dev-prod.md) | | `npm run harmonize:schema` | Apply dev migrations to prod schema only (dev → prod) | -| `npm run harmonize:db` / `harmonize:images` | DB or image merge only (`harmonize:images` also regenerates painting + portrait thumbs on both sides) | +| `npm run harmonize:db` / `harmonize:images` | DB or image merge only (`harmonize:images` also merges artists/paintings checkup flags + image paths, then regenerates thumbs on both sides) | | `npm run dev:backfill-updated-at` | Backfill catalog `updated_at` from image mtimes (dev) | **One-time split (recommended):** pgAdmin on dev PC → open [`db/split-dev-prod-pgadmin.sql`](../db/split-dev-prod-pgadmin.sql) → run each STEP on database `postgres`, then verify on `gallery_dev`. diff --git a/Documentation/data-and-images.md b/Documentation/data-and-images.md index 69245d3..d83e896 100644 --- a/Documentation/data-and-images.md +++ b/Documentation/data-and-images.md @@ -31,7 +31,7 @@ File names are sanitised `{Artist}_{Title}.{ext}`. The image service can redisco | **Development** | `./data/images/` in repo | Working copy on dev PC | | **Production** | `/mnt/BasePool/Applications/Gallery/data/images` on TrueNAS | SMB `\\192.168.10.122\Gallery\data\images` | -**One-direction promote:** `npm run devtoprod:images` (dev → prod, skip older). **Refresh all images on dev from prod:** `npm run prodto:dev:images`. **One artist only:** map the SMB share, then `robocopy` that artist’s `ArtistName*` files under `paintings/` and `paintings/thumbs/` (see [FAC.md](FAC.md#dev--prod-image-sync-smb)). **Bidirectional merge** (newer file wins, then rebuild painting + portrait thumbs on both sides): `npm run harmonize:images` or full `npm run harmonize` — see [harmonize-dev-prod.md](harmonize-dev-prod.md). General sync reference: [environments.md](environments.md). +**One-direction promote:** `npm run devtoprod:images` (dev → prod, skip older). **Refresh all images on dev from prod:** `npm run prodto:dev:images`. **One artist only:** map the SMB share, then `robocopy` that artist’s `ArtistName*` files under `paintings/` and `paintings/thumbs/` (see [FAC.md](FAC.md#dev--prod-image-sync-smb)). **Bidirectional merge** (newer file wins, then merge artists/paintings checkup flags + image paths, then rebuild painting + portrait thumbs on both sides): `npm run harmonize:images` or full `npm run harmonize` — see [harmonize-dev-prod.md](harmonize-dev-prod.md). General sync reference: [environments.md](environments.md). ## Scripts overview @@ -49,7 +49,7 @@ File names are sanitised `{Artist}_{Title}.{ext}`. The image service can redisco | `fetch-missing-images.js` | `npm run dev:fetch-images` | Downloads files for paintings missing on disk | | `image-fetcher.js` | *(library)* | Wikimedia / museum resolution used by fetch scripts and API | | `sync-images-to-prod.ps1` / `sync-images-from-prod.ps1` | `npm run devtoprod:images` / `npm run prodto:dev:images` | Robocopy via SMB `\\192.168.10.122\Gallery` | -| `harmonize-db.js` / `harmonize-images.js` | `npm run harmonize:db` / `harmonize:images` | Bidirectional merge by `updated_at` / file mtime; images step then rebuilds thumbs on both sides | +| `harmonize-db.js` / `harmonize-images.js` | `npm run harmonize:db` / `harmonize:images` | Bidirectional merge by `updated_at` / file mtime; images step also merges artists/paintings checkup flags + paths, then rebuilds thumbs on both sides | | `harmonize.ps1` | `npm run harmonize` | Orchestrator: backups, optional schema, DB + image merge | | `regenerate-thumbnails.js` | `npm run dev:regenerate-thumbnails` | Rebuild painting thumbs from full images via `sharp` | | `regenerate-portrait-thumbs.js` | `npm run dev:regenerate-portrait-thumbs` | Rebuild timeline portrait thumbs (~256px) and set `portrait_thumb_path` | diff --git a/Documentation/environments.md b/Documentation/environments.md index cc93548..9e5dae8 100644 --- a/Documentation/environments.md +++ b/Documentation/environments.md @@ -341,7 +341,7 @@ For **incremental** dev ↔ prod merge (both sides edited), use [harmonize-dev-p | `npm run devtoprod:db:restore` | Dev PC PowerShell | Restore into prod | | `npm run harmonize` | Dev PC PowerShell | Bidirectional catalog + image merge — [harmonize-dev-prod.md](harmonize-dev-prod.md) | | `npm run harmonize:db` | Dev PC PowerShell | DB merge only | -| `npm run harmonize:images` | Dev PC PowerShell | Image merge + regenerate thumbs on both sides | +| `npm run harmonize:images` | Dev PC PowerShell | Image merge + artists/paintings checkup/path sync + regenerate thumbs on both sides | ## Image sync diff --git a/Documentation/harmonize-dev-prod.md b/Documentation/harmonize-dev-prod.md index 3d7c8ac..b17e9e7 100644 --- a/Documentation/harmonize-dev-prod.md +++ b/Documentation/harmonize-dev-prod.md @@ -83,7 +83,7 @@ npm run harmonize:dry-run | `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, then regenerate painting + portrait thumbs on **dev and prod** | +| `images` | `harmonize:images` | Bidirectional file merge, then merge **artists/paintings** (checkup flags + image paths), then regenerate thumbs on **dev and prod** | | `verify` | curl `/api/bounds` | Optional smoke check | Reports are written to `db/SyncReports/harmonize_db_*.json` and `harmonize_images_*.json` (gitignored). @@ -98,9 +98,10 @@ Reports are written to `db/SyncReports/harmonize_db_*.json` and `harmonize_image | `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, then regenerate painting + portrait thumbnails on both sides | -| `npm run harmonize:images -- --skip-thumbnails` | File merge only (no thumb rebuild) | -| `npm run harmonize:images -- --dry-run` | Preview file copies only (skips thumbnails) | +| `npm run harmonize:images` | Merge image files by mtime, merge artists/paintings checkup flags + image paths, then regenerate thumbs on both sides | +| `npm run harmonize:images -- --skip-thumbnails` | File + checkup/path DB merge only (no thumb rebuild) | +| `npm run harmonize:images -- --skip-db` | File merge (+ thumbs) without artists/paintings checkup sync | +| `npm run harmonize:images -- --dry-run` | Preview file copies and catalog merge (skips thumbnails) | | `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 | diff --git a/client/src/components/VirtualGallery.css b/client/src/components/VirtualGallery.css index baa6368..d589962 100644 --- a/client/src/components/VirtualGallery.css +++ b/client/src/components/VirtualGallery.css @@ -419,3 +419,9 @@ width: 100%; margin-top: 8px; } + +.exit-nav-footer { + margin-top: 16px; + padding-top: 12px; + border-top: 1px solid rgba(201, 169, 110, 0.25); +} diff --git a/client/src/components/VirtualGallery.tsx b/client/src/components/VirtualGallery.tsx index 0106b9a..0ea4bba 100644 --- a/client/src/components/VirtualGallery.tsx +++ b/client/src/components/VirtualGallery.tsx @@ -110,6 +110,8 @@ const WALL_PADDING = 1.4; /** Every wall shows at most one row; side-wall depth grows to fit the catalog. */ const MAX_WALL_ROWS = 1; const DOOR_WIDTH = 2.4; +/** Minimum horizontal distance from walls, corners, door planes, and painting faces. */ +const PLAYER_CLEARANCE = 0.5; const DOOR_HEIGHT = 2.5; /** Museum exit — dimensions derived from door opening. */ const EXIT_JAMB = 0.13; @@ -176,6 +178,73 @@ interface HallLayout { segments: WallSegment[]; } +interface XZBounds { + minX: number; + maxX: number; + minZ: number; + maxZ: number; +} + +/** Inner wall face half-extents (room center → inside of wall box). */ +function innerWallHalfExtents(width: number, depth: number): { halfW: number; halfD: number } { + return { + halfW: width / 2 - WALL_THICKNESS / 2, + halfD: depth / 2 - WALL_THICKNESS / 2, + }; +} + +/** Keep-out box in front of a hanging painting (XZ), including player clearance. */ +function paintingKeepOutBounds(slot: FrameSlot): XZBounds { + const [px, , pz] = slot.position; + const halfAlong = slot.maxW / 2 + FRAME_MAT_BORDER + FRAME_RAIL + 0.04; + const intoRoom = FRAME_DEPTH + FRAME_FACE_Z + PLAYER_CLEARANCE; + if (slot.side === 'left') { + return { minX: px - 0.02, maxX: px + intoRoom, minZ: pz - halfAlong, maxZ: pz + halfAlong }; + } + if (slot.side === 'right') { + return { minX: px - intoRoom, maxX: px + 0.02, minZ: pz - halfAlong, maxZ: pz + halfAlong }; + } + // back wall — faces into the room (+Z) + return { minX: px - halfAlong, maxX: px + halfAlong, minZ: pz - 0.02, maxZ: pz + intoRoom }; +} + +/** Door jamb keep-outs at an opening in the front (+Z) or back (−Z) wall. */ +function doorJambKeepOuts(halfD: number, atFront: boolean): XZBounds[] { + const jambZ = atFront ? halfD : -halfD; + const along = PLAYER_CLEARANCE; + const intoRoom = PLAYER_CLEARANCE; + const leftX = -DOOR_WIDTH / 2; + const rightX = DOOR_WIDTH / 2; + if (atFront) { + return [ + { minX: leftX - along, maxX: leftX + along, minZ: jambZ - intoRoom, maxZ: jambZ + 0.02 }, + { minX: rightX - along, maxX: rightX + along, minZ: jambZ - intoRoom, maxZ: jambZ + 0.02 }, + ]; + } + return [ + { minX: leftX - along, maxX: leftX + along, minZ: jambZ - 0.02, maxZ: jambZ + intoRoom }, + { minX: rightX - along, maxX: rightX + along, minZ: jambZ - 0.02, maxZ: jambZ + intoRoom }, + ]; +} + +function pointInBounds(x: number, z: number, b: XZBounds): boolean { + return x > b.minX && x < b.maxX && z > b.minZ && z < b.maxZ; +} + +/** Push a point out of an AABB via the shortest axis (XZ). */ +function pushOutOfBounds(pos: { x: number; z: number }, b: XZBounds): void { + if (!pointInBounds(pos.x, pos.z, b)) return; + const dxMin = pos.x - b.minX; + const dxMax = b.maxX - pos.x; + const dzMin = pos.z - b.minZ; + const dzMax = b.maxZ - pos.z; + const m = Math.min(dxMin, dxMax, dzMin, dzMax); + if (m === dxMin) pos.x = b.minX; + else if (m === dxMax) pos.x = b.maxX; + else if (m === dzMin) pos.z = b.minZ; + else pos.z = b.maxZ; +} + function paintingIsReviewed(painting: Painting): boolean { return !!painting.checkup_checked; } @@ -1749,7 +1818,7 @@ function MovementHallNavPanel({ ))} @@ -1762,11 +1831,13 @@ function NavigationPanel({ navigation, loading, onSelect, + onExitTimeline, onClose, }: { navigation: ArtistNavigation | null; loading: boolean; onSelect: (artistId: number) => void; + onExitTimeline: () => void; onClose: () => void; }) { const renderColumn = (title: string, groups: MovementArtistGroup[], emptyHint: string) => ( @@ -1822,6 +1893,11 @@ function NavigationPanel({ 'No documented successors via painting influences.' )} +
+ +
); @@ -2017,11 +2093,62 @@ export default function VirtualGallery(props: Props) { const hasNextHall = isWingedHall && movementHalls.length > 1 && hallIndex < movementHalls.length - 1; - const halfW = layout.width / 2 - 0.55; - const halfD = layout.depth / 2 - 0.35; - const exitZ = layout.depth / 2 - 0.55; + const { halfW: wallInnerHalfW, halfD: wallInnerHalfD } = useMemo( + () => innerWallHalfExtents(layout.width, layout.depth), + [layout.width, layout.depth] + ); + /** Playable half-extents: 0.5 m clear of inner wall faces (corners included). */ + const playHalfW = wallInnerHalfW - PLAYER_CLEARANCE; + const playHalfD = wallInnerHalfD - PLAYER_CLEARANCE; + const exitZ = playHalfD; const fogFar = Math.max(55, layout.depth + 42); + const collisionObstacles = useMemo(() => { + const boxes: XZBounds[] = []; + for (const seg of layout.segments) { + for (const slot of seg.slots) boxes.push(paintingKeepOutBounds(slot)); + } + // Door jambs — keep 0.5 m from the opening posts; opening corridor stays walkable. + boxes.push(...doorJambKeepOuts(wallInnerHalfD, true)); + if (isWingedHall) boxes.push(...doorJambKeepOuts(wallInnerHalfD, false)); + return boxes; + }, [layout.segments, wallInnerHalfD, isWingedHall]); + + const resolvePlayerPosition = useCallback( + (pos: { x: number; z: number }, allowFrontDoorApproach: boolean) => { + const clampToWalls = () => { + pos.x = Math.max(-playHalfW, Math.min(playHalfW, pos.x)); + const inDoorBand = Math.abs(pos.x) < DOOR_WIDTH / 2 - PLAYER_CLEARANCE * 0.35; + if (isWingedHall) { + let minZ = -playHalfD; + let maxZ = playHalfD; + if (inDoorBand) { + minZ = -wallInnerHalfD + 0.08; + if (hasNextHall || allowFrontDoorApproach) maxZ = wallInnerHalfD - 0.08; + } + pos.z = Math.max(minZ, Math.min(maxZ, pos.z)); + } else { + let maxZ = exitZ; + if (inDoorBand && allowFrontDoorApproach) maxZ = wallInnerHalfD - 0.08; + pos.z = Math.max(-playHalfD, Math.min(maxZ, pos.z)); + } + }; + + clampToWalls(); + for (const box of collisionObstacles) pushOutOfBounds(pos, box); + clampToWalls(); + }, + [ + playHalfW, + playHalfD, + exitZ, + isWingedHall, + hasNextHall, + wallInnerHalfD, + collisionObstacles, + ] + ); + const initialPos = useMemo( () => new THREE.Vector3(0, EYE_HEIGHT, layout.depth / 2 - 2.2), [layout.depth] @@ -2091,8 +2218,24 @@ export default function VirtualGallery(props: Props) { } }, [isWingedHall, onBack, !isWingedHall ? props.data.artist.id : undefined]); - const backExitZ = isWingedHall ? -layout.depth / 2 + 0.55 : layout.depth / 2 - 0.55; - const frontPassageZ = layout.depth / 2 - 0.55; + const backExitZ = isWingedHall ? -playHalfD : exitZ; + const frontPassageZ = playHalfD; + + const updateProximityFlags = useCallback( + (pos: { x: number; z: number }) => { + if (isWingedHall) { + const atBackExit = pos.z < backExitZ + 0.8 && Math.abs(pos.x) < DOOR_WIDTH / 2 + 0.6; + const atFrontPassage = + hasNextHall && pos.z > frontPassageZ - 0.8 && Math.abs(pos.x) < DOOR_WIDTH / 2 + 0.6; + setNearExit(atBackExit); + setNearPassage(!!atFrontPassage); + } else { + const atExit = pos.z > exitZ - 0.8 && Math.abs(pos.x) < DOOR_WIDTH / 2 + 0.6; + setNearExit(atExit); + } + }, + [isWingedHall, backExitZ, frontPassageZ, hasNextHall, exitZ] + ); const moveCamera = useCallback( (forward: number, strafe: number, rotY: number) => { @@ -2100,43 +2243,49 @@ export default function VirtualGallery(props: Props) { const target = camTargetRef.current.clone(); const angle = Math.atan2(target.x - pos.x, target.z - pos.z); + // Turn in place: never move; only change look direction. if (rotY !== 0) { const newAngle = angle + rotY; - const dist = pos.distanceTo(target); + const dist = Math.max(0.5, pos.distanceTo(target)); target.x = pos.x + Math.sin(newAngle) * dist; target.z = pos.z + Math.cos(newAngle) * dist; - } else { - const newAngle = angle; - pos.x += Math.sin(newAngle) * forward + Math.sin(newAngle + Math.PI / 2) * strafe; - pos.z += Math.cos(newAngle) * forward + Math.cos(newAngle + Math.PI / 2) * strafe; - target.x += Math.sin(newAngle) * forward + Math.sin(newAngle + Math.PI / 2) * strafe; - target.z += Math.cos(newAngle) * forward + Math.cos(newAngle + Math.PI / 2) * strafe; + levelHorizontalView(pos, target); + updateProximityFlags(pos); + setCamPos(pos); + setCamTarget(target); + return; } - pos.x = Math.max(-halfW, Math.min(halfW, pos.x)); - target.x = Math.max(-halfW, Math.min(halfW, target.x)); - - if (isWingedHall) { - pos.z = Math.max(-halfD, Math.min(halfD, pos.z)); - target.z = Math.max(-halfD, Math.min(halfD, target.z)); - const atBackExit = pos.z < backExitZ + 0.8 && Math.abs(pos.x) < DOOR_WIDTH / 2 + 0.6; - const atFrontPassage = - hasNextHall && pos.z > frontPassageZ - 0.8 && Math.abs(pos.x) < DOOR_WIDTH / 2 + 0.6; - setNearExit(atBackExit); - setNearPassage(!!atFrontPassage); - } else { - pos.z = Math.max(-halfD, Math.min(exitZ, pos.z)); - target.z = Math.max(-halfD, Math.min(exitZ, target.z)); - const atExit = pos.z > exitZ - 0.8 && Math.abs(pos.x) < DOOR_WIDTH / 2 + 0.6; - setNearExit(atExit); + const dx = Math.sin(angle) * forward + Math.sin(angle + Math.PI / 2) * strafe; + const dz = Math.cos(angle) * forward + Math.cos(angle + Math.PI / 2) * strafe; + if (dx === 0 && dz === 0) { + updateProximityFlags(pos); + return; } + const proposed = { x: pos.x + dx, z: pos.z + dz }; + const resolved = { x: proposed.x, z: proposed.z }; + resolvePlayerPosition(resolved, true); + + // Hit wall/painting/door jamb: cancel the whole step — no slide, no view change. + if ( + Math.abs(resolved.x - proposed.x) > 1e-4 || + Math.abs(resolved.z - proposed.z) > 1e-4 + ) { + updateProximityFlags(pos); + return; + } + + pos.x = proposed.x; + pos.z = proposed.z; + target.x += dx; + target.z += dz; levelHorizontalView(pos, target); - + updateProximityFlags(pos); setCamPos(pos); setCamTarget(target); }, - [halfW, halfD, exitZ, isWingedHall, backExitZ, frontPassageZ, hasNextHall] + [resolvePlayerPosition, updateProximityFlags] ); useEffect(() => { @@ -2362,6 +2511,7 @@ export default function VirtualGallery(props: Props) { navigation={navigation} loading={navLoading} onSelect={handleNavigate} + onExitTimeline={onBack} onClose={() => setShowExitNav(false)} /> )} @@ -2407,7 +2557,7 @@ export default function VirtualGallery(props: Props) { ) : ( <>
  • Golden lamps mark works linked in the influence graph
  • -
  • Click the exit door or E to visit related artists
  • +
  • Click the exit door or E for related artists or back to the timeline
  • )} diff --git a/scripts/harmonize-images.js b/scripts/harmonize-images.js index b14e1f1..4baa77d 100644 --- a/scripts/harmonize-images.js +++ b/scripts/harmonize-images.js @@ -1,11 +1,13 @@ /** * Bidirectional image harmonize (dev <-> prod) by file mtime, + * then merge artists/paintings catalog rows (checkup flags + image paths), * then regenerate painting + portrait thumbnails on both image roots. * * Usage: * npm run harmonize:images * npm run harmonize:images -- --dry-run * npm run harmonize:images -- --skip-thumbnails + * npm run harmonize:images -- --skip-db */ const fs = require('fs'); const path = require('path'); @@ -17,6 +19,9 @@ const { printCliResult } = require('./lib/cli-result'); const IMAGE_SUBDIRS = ['portraits', 'paintings']; const MTIME_TOLERANCE_MS = 1000; +/** Tables that carry checkup flags and image/portrait path links. */ +const IMAGE_CATALOG_TABLES = ['artists', 'paintings']; + const THUMB_SCRIPTS = [ 'regenerate-thumbnails.js', 'regenerate-portrait-thumbs.js', @@ -24,10 +29,13 @@ const THUMB_SCRIPTS = [ function parseArgs(argv) { const configArg = argv.find((a) => a.startsWith('--config=')); + const preferArg = argv.find((a) => a.startsWith('--prefer=')); return { dryRun: argv.includes('--dry-run'), verbose: argv.includes('--verbose'), skipThumbnails: argv.includes('--skip-thumbnails'), + skipDb: argv.includes('--skip-db'), + prefer: preferArg ? preferArg.slice('--prefer='.length).trim().toLowerCase() : null, configPath: configArg ? configArg.slice('--config='.length) : null, }; } @@ -117,6 +125,35 @@ function regenerateThumbnails({ label, imageDir, database }) { } } +/** + * Merge artists + paintings so checkup flags and image/portrait path links + * travel with the file sync (harmonize:images alone used to leave DB stale). + */ +function syncImageCatalogRows({ dryRun, prefer, verbose }) { + console.log('\n--- Catalog (artists + paintings checkup / image paths) ---'); + const args = [ + path.join(__dirname, 'harmonize-db.js'), + `--tables=${IMAGE_CATALOG_TABLES.join(',')}`, + ]; + if (dryRun) args.push('--dry-run'); + if (verbose) args.push('--verbose'); + if (prefer === 'dev' || prefer === 'prod') args.push(`--prefer=${prefer}`); + + const result = spawnSync(process.execPath, args, { + cwd: rootDir, + stdio: 'inherit', + env: { + ...process.env, + // Parent already confirmed prod writes for this harmonize:images run. + CONFIRM_PROD: '1', + }, + }); + const code = result.status ?? 1; + if (code !== 0) { + throw new Error(`Image catalog sync failed: harmonize-db.js exited ${code}`); + } +} + function timestampSlug(date = new Date()) { const pad = (n) => String(n).padStart(2, '0'); return [ @@ -146,7 +183,7 @@ async function main() { if (!dryRun && process.env.CONFIRM_PROD !== '1') { await confirmProdAction( - 'Harmonize will copy image files between dev and prod, then regenerate painting and portrait thumbnails on both sides (prod files and gallery_prod paths may be updated).', + 'Harmonize will copy image files between dev and prod, merge artists/paintings checkup flags and image paths, then regenerate thumbnails on both sides (prod files and gallery_prod may be updated).', ); } @@ -205,7 +242,23 @@ async function main() { fs.mkdirSync(reportDir, { recursive: true }); const reportPath = path.join(reportDir, `harmonize_images_${timestampSlug()}.json`); - const thumbDetails = []; + const extraDetails = []; + + if (!options.skipDb) { + syncImageCatalogRows({ + dryRun, + prefer: options.prefer, + verbose: options.verbose, + }); + extraDetails.push( + dryRun + ? 'Catalog checkup/path sync previewed (artists, paintings; dry-run)' + : 'Catalog checkup flags + image paths merged (artists, paintings)', + ); + } else { + extraDetails.push('Catalog checkup/path sync skipped (--skip-db)'); + } + if (!dryRun && !options.skipThumbnails) { require('dotenv').config({ path: path.join(rootDir, '.env') }); const devDb = process.env.DB_NAME || 'gallery_dev'; @@ -215,24 +268,26 @@ async function main() { imageDir: devDir, database: devDb, }); - thumbDetails.push(`Dev thumbs regenerated (${devDb} @ ${devDir})`); + extraDetails.push(`Dev thumbs regenerated (${devDb} @ ${devDir})`); regenerateThumbnails({ label: 'prod', imageDir: prodDir, database: PROD_DB_NAME, }); - thumbDetails.push(`Prod thumbs regenerated (${PROD_DB_NAME} @ ${prodDir})`); + extraDetails.push(`Prod thumbs regenerated (${PROD_DB_NAME} @ ${prodDir})`); } else if (options.skipThumbnails) { - thumbDetails.push('Thumbnails skipped (--skip-thumbnails)'); + extraDetails.push('Thumbnails skipped (--skip-thumbnails)'); } else { - thumbDetails.push('Thumbnails skipped (dry-run)'); + extraDetails.push('Thumbnails skipped (dry-run)'); } fs.writeFileSync(reportPath, JSON.stringify({ generatedAt: new Date().toISOString(), dryRun, skipThumbnails: options.skipThumbnails, + skipDb: options.skipDb, + prefer: options.prefer, devDir, prodDir, stats: { @@ -240,7 +295,7 @@ async function main() { prodToDev: stats.prodToDev, skipped: stats.skipped, }, - thumbnails: thumbDetails, + steps: extraDetails, actions: options.verbose ? stats.actions : stats.actions.slice(0, 500), }, null, 2)); @@ -253,7 +308,7 @@ async function main() { `Dev: ${devDir}`, `Prod: ${prodDir}`, `Report: ${reportPath}`, - ...thumbDetails, + ...extraDetails, ], }); }