Fix debug upload persistence and UX; exclude prod audit log from DB restore

Uploads and fixes now bust browser cache via file-mtime keys in API
responses. Debug upload shows a centered loading overlay and blocks search
while uploading. Prod DB restore skips curator_audit_log.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-07-09 18:30:19 +03:00
co-authored by Cursor
parent 62096e8210
commit f78c14f307
18 changed files with 506 additions and 154 deletions
+68 -7
View File
@@ -13,6 +13,59 @@ const FETCH_TIMEOUT_MS = 15000;
const PORTRAIT_THUMB_WIDTH = 256;
const inflight = new Map();
function imageFileCacheKey(relPath) {
if (!relPath || typeof relPath !== 'string') return null;
const abs = path.join(IMAGE_DIR, relPath.replace(/^\//, ''));
try {
if (!fs.existsSync(abs)) return null;
return Math.floor(fs.statSync(abs).mtimeMs);
} catch {
return null;
}
}
function enrichPaintingRow(row) {
if (!row || typeof row !== 'object') return row;
return {
...row,
image_cache_key: imageFileCacheKey(row.image_path),
thumbnail_cache_key: imageFileCacheKey(row.thumbnail_path),
};
}
function enrichArtistRow(row) {
if (!row || typeof row !== 'object') return row;
return {
...row,
portrait_cache_key: imageFileCacheKey(row.portrait_path),
portrait_thumb_cache_key: imageFileCacheKey(row.portrait_thumb_path),
};
}
function paintingImagePayload(paths, extra = {}) {
const imagePath = paths.imagePath ?? paths.image_path ?? null;
const thumbnailPath = paths.thumbnailPath ?? paths.thumbnail_path ?? null;
return {
imagePath,
thumbnailPath,
image_cache_key: imageFileCacheKey(imagePath),
thumbnail_cache_key: imageFileCacheKey(thumbnailPath),
...extra,
};
}
function artistPortraitPayload(paths, extra = {}) {
const portraitPath = paths.portraitPath ?? paths.portrait_path ?? null;
const portraitThumbPath = paths.portraitThumbPath ?? paths.portrait_thumb_path ?? null;
return {
portraitPath,
portraitThumbPath,
portrait_cache_key: imageFileCacheKey(portraitPath),
portrait_thumb_cache_key: imageFileCacheKey(portraitThumbPath),
...extra,
};
}
function safePaintingBase(artistName, title) {
return `${artistName}_${title}`.replace(/[^a-zA-Z0-9_-]/g, '_');
}
@@ -269,7 +322,7 @@ async function clearPaintingImage(paintingId) {
[paintingId]
);
return { imagePath: null, thumbnailPath: null };
return paintingImagePayload({ imagePath: null, thumbnailPath: null });
}
async function clearArtistPortrait(artistId) {
@@ -290,7 +343,7 @@ async function clearArtistPortrait(artistId) {
[artistId]
);
return { portraitPath: null, portraitThumbPath: null };
return artistPortraitPayload({ portraitPath: null, portraitThumbPath: null });
}
async function replacePaintingImageFromBuffer(paintingId, buffer, mimeType) {
@@ -344,7 +397,7 @@ async function replacePaintingImageFromBuffer(paintingId, buffer, mimeType) {
[imagePath, thumbnailPath, paintingId]
);
return { imagePath, thumbnailPath };
return paintingImagePayload({ imagePath, thumbnailPath });
}
async function replaceArtistPortraitFromBuffer(artistId, buffer, mimeType) {
@@ -387,12 +440,14 @@ async function replaceArtistPortraitFromBuffer(artistId, buffer, mimeType) {
fs.writeFileSync(fullDest, buffer);
const portraitPath = path.join('portraits', safeBase + fullExt).replace(/\\/g, '/');
const portraitThumbPath = (await writePortraitThumb(fullDest, safeBase)) || portraitPath;
return updateArtistPortraitPaths(artistId, portraitPath, portraitThumbPath);
const updated = await updateArtistPortraitPaths(artistId, portraitPath, portraitThumbPath);
return artistPortraitPayload(updated);
}
const portraitPath = path.join('portraits', safeBase + '.jpg').replace(/\\/g, '/');
const portraitThumbPath = (await writePortraitThumb(jpgDest, safeBase)) || portraitPath;
return updateArtistPortraitPaths(artistId, portraitPath, portraitThumbPath);
const updated = await updateArtistPortraitPaths(artistId, portraitPath, portraitThumbPath);
return artistPortraitPayload(updated);
}
async function replacePaintingImageFromUrl(paintingId, imageUrl, context = {}) {
@@ -436,7 +491,7 @@ async function replacePaintingImageFromUrl(paintingId, imageUrl, context = {}) {
[imagePath, thumbnailPath, paintingId]
);
return { imagePath, thumbnailPath };
return paintingImagePayload({ imagePath, thumbnailPath });
}
async function replaceArtistPortraitFromUrl(artistId, imageUrl, context = {}) {
@@ -480,7 +535,8 @@ async function replaceArtistPortraitFromUrl(artistId, imageUrl, context = {}) {
const sourceForThumb = fs.existsSync(jpgDest) ? jpgDest : path.join(IMAGE_DIR, portraitPath);
const portraitThumbPath = (await writePortraitThumb(sourceForThumb, safeBase)) || portraitPath;
return updateArtistPortraitPaths(artistId, portraitPath, portraitThumbPath);
const updated = await updateArtistPortraitPaths(artistId, portraitPath, portraitThumbPath);
return artistPortraitPayload(updated);
}
module.exports = {
@@ -493,5 +549,10 @@ module.exports = {
clearArtistPortrait,
replacePaintingImageFromBuffer,
replaceArtistPortraitFromBuffer,
enrichPaintingRow,
enrichArtistRow,
paintingImagePayload,
artistPortraitPayload,
imageFileCacheKey,
IMAGE_DIR,
};