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:
co-authored by
Cursor
parent
62096e8210
commit
f78c14f307
+102
-29
@@ -52,36 +52,63 @@ export async function logoutCurator(): Promise<void> {
|
||||
if (!res.ok) throw new Error(`Logout failed: ${res.status}`);
|
||||
}
|
||||
|
||||
export function imageUrl(path: string | null | undefined): string {
|
||||
if (!path) return '/placeholder-art.svg';
|
||||
return `/images/${path}`;
|
||||
}
|
||||
|
||||
export function portraitUrl(path: string | null | undefined, revision?: number): string {
|
||||
const base = imageUrl(path);
|
||||
export function imageUrl(path: string | null | undefined, revision?: number | null): string {
|
||||
const base = !path ? '/placeholder-art.svg' : `/images/${path}`;
|
||||
if (!revision || base.startsWith('/placeholder')) return base;
|
||||
return `${base}${base.includes('?') ? '&' : '?'}v=${revision}`;
|
||||
}
|
||||
|
||||
/** Small portrait for timeline / movement flow (~256px). Falls back to full portrait. */
|
||||
export function portraitUrl(
|
||||
path: string | null | undefined,
|
||||
revision?: number | null,
|
||||
options?: { portrait_cache_key?: number | null; portrait_thumb_cache_key?: number | null }
|
||||
): string {
|
||||
const cacheRevision =
|
||||
revision ?? options?.portrait_cache_key ?? options?.portrait_thumb_cache_key ?? undefined;
|
||||
return imageUrl(path, cacheRevision);
|
||||
}
|
||||
|
||||
export function portraitThumbUrl(
|
||||
artist: {
|
||||
portrait_thumb_path?: string | null;
|
||||
portrait_path?: string | null;
|
||||
portrait_cache_key?: number | null;
|
||||
portrait_thumb_cache_key?: number | null;
|
||||
},
|
||||
revision?: number
|
||||
revision?: number | null
|
||||
): string {
|
||||
const path = artist.portrait_thumb_path || artist.portrait_path;
|
||||
return portraitUrl(path, revision);
|
||||
const cacheRevision =
|
||||
revision ?? artist.portrait_thumb_cache_key ?? artist.portrait_cache_key ?? undefined;
|
||||
return portraitUrl(path, cacheRevision);
|
||||
}
|
||||
|
||||
export function paintingImageRevision(
|
||||
painting: {
|
||||
image_cache_key?: number | null;
|
||||
thumbnail_cache_key?: number | null;
|
||||
},
|
||||
sessionRevision?: number | null
|
||||
): number | undefined {
|
||||
const apiRevision = painting.image_cache_key ?? painting.thumbnail_cache_key;
|
||||
if (apiRevision != null) return apiRevision;
|
||||
if (sessionRevision != null) return sessionRevision;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Image for 3D gallery — prefer thumbnail for faster texture loads */
|
||||
export function galleryImageUrl(painting: {
|
||||
thumbnail_path?: string | null;
|
||||
image_path?: string | null;
|
||||
}): string | null {
|
||||
if (painting.thumbnail_path) return `/images/${painting.thumbnail_path}`;
|
||||
if (painting.image_path) return `/images/${painting.image_path}`;
|
||||
export function galleryImageUrl(
|
||||
painting: {
|
||||
thumbnail_path?: string | null;
|
||||
image_path?: string | null;
|
||||
image_cache_key?: number | null;
|
||||
thumbnail_cache_key?: number | null;
|
||||
},
|
||||
sessionRevision?: number | null
|
||||
): string | null {
|
||||
const revision = paintingImageRevision(painting, sessionRevision);
|
||||
if (painting.thumbnail_path) return imageUrl(painting.thumbnail_path, revision);
|
||||
if (painting.image_path) return imageUrl(painting.image_path, revision);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -90,27 +117,35 @@ export function galleryImageUrlWithRevision(
|
||||
id?: number;
|
||||
thumbnail_path?: string | null;
|
||||
image_path?: string | null;
|
||||
image_cache_key?: number | null;
|
||||
thumbnail_cache_key?: number | null;
|
||||
},
|
||||
revision?: number
|
||||
sessionRevision?: number | null
|
||||
): string | null {
|
||||
const base = galleryImageUrl(painting);
|
||||
if (!base || !revision) return base;
|
||||
return `${base}${base.includes('?') ? '&' : '?'}v=${revision}`;
|
||||
return galleryImageUrl(painting, sessionRevision);
|
||||
}
|
||||
|
||||
export function paintingImageUrl(painting: {
|
||||
id: number;
|
||||
image_path?: string | null;
|
||||
thumbnail_path?: string | null;
|
||||
checkup_fixed?: boolean;
|
||||
}): string | null {
|
||||
if (painting.image_path) return `/images/${painting.image_path}`;
|
||||
if (painting.thumbnail_path) return `/images/${painting.thumbnail_path}`;
|
||||
export function paintingImageUrl(
|
||||
painting: {
|
||||
id: number;
|
||||
image_path?: string | null;
|
||||
thumbnail_path?: string | null;
|
||||
checkup_fixed?: boolean;
|
||||
image_cache_key?: number | null;
|
||||
thumbnail_cache_key?: number | null;
|
||||
},
|
||||
sessionRevision?: number | null
|
||||
): string | null {
|
||||
const revision = paintingImageRevision(painting, sessionRevision);
|
||||
if (painting.image_path) return imageUrl(painting.image_path, revision);
|
||||
if (painting.thumbnail_path) return imageUrl(painting.thumbnail_path, revision);
|
||||
if (painting.checkup_fixed) return null;
|
||||
return `/api/paintings/${painting.id}/image?size=full`;
|
||||
}
|
||||
|
||||
async function fileToBase64Payload(file: File): Promise<{ imageData: string; mimeType: string }> {
|
||||
validateDebugUploadFile(file);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
@@ -122,7 +157,7 @@ async function fileToBase64Payload(file: File): Promise<{ imageData: string; mim
|
||||
const comma = result.indexOf(',');
|
||||
resolve({
|
||||
imageData: comma >= 0 ? result.slice(comma + 1) : result,
|
||||
mimeType: file.type || 'image/jpeg',
|
||||
mimeType: file.type || mimeTypeFromFilename(file.name) || 'image/jpeg',
|
||||
});
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Could not read file'));
|
||||
@@ -130,6 +165,39 @@ async function fileToBase64Payload(file: File): Promise<{ imageData: string; mim
|
||||
});
|
||||
}
|
||||
|
||||
function mimeTypeFromFilename(filename: string): string | null {
|
||||
const ext = filename.toLowerCase().match(/\.([a-z0-9]+)$/)?.[1];
|
||||
switch (ext) {
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
return 'image/jpeg';
|
||||
case 'png':
|
||||
return 'image/png';
|
||||
case 'webp':
|
||||
return 'image/webp';
|
||||
case 'gif':
|
||||
return 'image/gif';
|
||||
case 'avif':
|
||||
return 'image/avif';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function validateDebugUploadFile(file: File): void {
|
||||
const maxBytes = 15 * 1024 * 1024;
|
||||
if (file.size <= 0) {
|
||||
throw new Error('Selected file is empty.');
|
||||
}
|
||||
if (file.size > maxBytes) {
|
||||
throw new Error('Image too large (max 15 MB).');
|
||||
}
|
||||
const nameOk = /\.(jpe?g|png|webp|gif|avif)$/i.test(file.name);
|
||||
if (!file.type.startsWith('image/') && !nameOk) {
|
||||
throw new Error('Please choose an image file (JPEG, PNG, WebP, GIF).');
|
||||
}
|
||||
}
|
||||
|
||||
async function postJsonImageAction<T>(url: string, payload: { imageData: string; mimeType: string }): Promise<T> {
|
||||
const res = await fetch(url, {
|
||||
...fetchCredentials,
|
||||
@@ -156,12 +224,17 @@ export async function preloadArtistImages(artistId: number): Promise<{ fetched:
|
||||
export interface FixPaintingImageResult {
|
||||
imagePath: string | null;
|
||||
thumbnailPath: string | null;
|
||||
image_cache_key?: number | null;
|
||||
thumbnail_cache_key?: number | null;
|
||||
fixed?: boolean;
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
export interface FixArtistPortraitResult {
|
||||
portraitPath: string | null;
|
||||
portraitThumbPath?: string | null;
|
||||
portrait_cache_key?: number | null;
|
||||
portrait_thumb_cache_key?: number | null;
|
||||
fixed?: boolean;
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user