Sync checkup flags with image harmonize; tighten gallery collision and exit-to-timeline.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-07-27 14:33:36 +03:00
co-authored by Cursor
parent 41d2d5d844
commit 67594ea4d3
7 changed files with 260 additions and 48 deletions
+63 -8
View File
@@ -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,
],
});
}