Add debug More/Clear/Upload tools for paintings and artist portraits.
Extends the debug panel on painting detail and artist bio with a 20-result search picker, local image upload, and clear-to-empty-frame workflow, plus API routes, artist checkup migration, and documentation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
b4425445bb
commit
0972b5df99
+134
-3
@@ -20,6 +20,12 @@ export function imageUrl(path: string | null | undefined): string {
|
||||
return `/images/${path}`;
|
||||
}
|
||||
|
||||
export function portraitUrl(path: string | null | undefined, revision?: number): string {
|
||||
const base = imageUrl(path);
|
||||
if (!revision || base.startsWith('/placeholder')) return base;
|
||||
return `${base}${base.includes('?') ? '&' : '?'}v=${revision}`;
|
||||
}
|
||||
|
||||
/** Image for 3D gallery — local cached files only (API fetch is too slow for realtime 3D) */
|
||||
export function galleryImageUrl(painting: {
|
||||
thumbnail_path?: string | null;
|
||||
@@ -47,12 +53,47 @@ export function paintingImageUrl(painting: {
|
||||
id: number;
|
||||
image_path?: string | null;
|
||||
thumbnail_path?: string | null;
|
||||
}): string {
|
||||
checkup_fixed?: boolean;
|
||||
}): string | null {
|
||||
if (painting.image_path) return `/images/${painting.image_path}`;
|
||||
if (painting.thumbnail_path) return `/images/${painting.thumbnail_path}`;
|
||||
if (painting.checkup_fixed) return null;
|
||||
return `/api/paintings/${painting.id}/image?size=full`;
|
||||
}
|
||||
|
||||
async function fileToBase64Payload(file: File): Promise<{ imageData: string; mimeType: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = reader.result;
|
||||
if (typeof result !== 'string') {
|
||||
reject(new Error('Could not read file'));
|
||||
return;
|
||||
}
|
||||
const comma = result.indexOf(',');
|
||||
resolve({
|
||||
imageData: comma >= 0 ? result.slice(comma + 1) : result,
|
||||
mimeType: file.type || 'image/jpeg',
|
||||
});
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Could not read file'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
async function postJsonImageAction<T>(url: string, payload: { imageData: string; mimeType: string }): Promise<T> {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Request failed: ${res.status}`);
|
||||
}
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export async function preloadArtistImages(artistId: number): Promise<{ fetched: number; total: number }> {
|
||||
const res = await fetch(`${API}/artists/${artistId}/preload-images`, { method: 'POST' });
|
||||
if (!res.ok) throw new Error('Preload failed');
|
||||
@@ -60,8 +101,14 @@ export async function preloadArtistImages(artistId: number): Promise<{ fetched:
|
||||
}
|
||||
|
||||
export interface FixPaintingImageResult {
|
||||
imagePath: string;
|
||||
thumbnailPath: string;
|
||||
imagePath: string | null;
|
||||
thumbnailPath: string | null;
|
||||
fixed?: boolean;
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
export interface FixArtistPortraitResult {
|
||||
portraitPath: string | null;
|
||||
fixed?: boolean;
|
||||
checked?: boolean;
|
||||
}
|
||||
@@ -75,6 +122,20 @@ export interface DebugImageSearchResult {
|
||||
thumbUrl?: string;
|
||||
}
|
||||
|
||||
export interface DebugImageSearchResultItem {
|
||||
imageUrl: string;
|
||||
thumbUrl?: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface DebugImageSearchManyResult {
|
||||
query: string;
|
||||
searchUrl: string;
|
||||
source: string;
|
||||
sourceLabel?: string;
|
||||
results: DebugImageSearchResultItem[];
|
||||
}
|
||||
|
||||
export interface PaintingCheckupRow {
|
||||
id: number;
|
||||
title: string;
|
||||
@@ -120,6 +181,9 @@ export const api = {
|
||||
getPaintingDebugImageSearch: (id: number) =>
|
||||
fetchJson<DebugImageSearchResult>(`${API}/paintings/${id}/debug-image-search`),
|
||||
|
||||
getPaintingDebugImageSearchMore: (id: number, limit = 20) =>
|
||||
fetchJson<DebugImageSearchManyResult>(`${API}/paintings/${id}/debug-image-search/more?limit=${limit}`),
|
||||
|
||||
fixPaintingImage: (
|
||||
id: number,
|
||||
imageUrl: string,
|
||||
@@ -137,6 +201,20 @@ export const api = {
|
||||
return res.json() as Promise<FixPaintingImageResult>;
|
||||
}),
|
||||
|
||||
clearPaintingImage: (id: number) =>
|
||||
fetch(`${API}/paintings/${id}/clear-image`, { method: 'POST' }).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Clear failed: ${res.status}`);
|
||||
}
|
||||
return res.json() as Promise<FixPaintingImageResult>;
|
||||
}),
|
||||
|
||||
uploadPaintingImage: async (id: number, file: File) => {
|
||||
const payload = await fileToBase64Payload(file);
|
||||
return postJsonImageAction<FixPaintingImageResult>(`${API}/paintings/${id}/upload-image`, payload);
|
||||
},
|
||||
|
||||
getPaintingCheckup: () => fetchJson<PaintingCheckupData>(`${API}/paintings/checkup`),
|
||||
|
||||
updatePaintingCheckupFlags: (
|
||||
@@ -154,6 +232,59 @@ export const api = {
|
||||
}
|
||||
return res.json() as Promise<{ checked: boolean; fixed: boolean }>;
|
||||
}),
|
||||
|
||||
getArtistDebugPortraitSearch: (id: number) =>
|
||||
fetchJson<DebugImageSearchResult>(`${API}/artists/${id}/debug-portrait-search`),
|
||||
|
||||
getArtistDebugPortraitSearchMore: (id: number, limit = 20) =>
|
||||
fetchJson<DebugImageSearchManyResult>(`${API}/artists/${id}/debug-portrait-search/more?limit=${limit}`),
|
||||
|
||||
fixArtistPortrait: (
|
||||
id: number,
|
||||
imageUrl: string,
|
||||
context?: { searchUrl?: string; source?: string; pageUrl?: string; thumbUrl?: string }
|
||||
) =>
|
||||
fetch(`${API}/artists/${id}/fix-portrait`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ imageUrl, ...context }),
|
||||
}).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Fix failed: ${res.status}`);
|
||||
}
|
||||
return res.json() as Promise<FixArtistPortraitResult>;
|
||||
}),
|
||||
|
||||
clearArtistPortrait: (id: number) =>
|
||||
fetch(`${API}/artists/${id}/clear-portrait`, { method: 'POST' }).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Clear failed: ${res.status}`);
|
||||
}
|
||||
return res.json() as Promise<FixArtistPortraitResult>;
|
||||
}),
|
||||
|
||||
uploadArtistPortrait: async (id: number, file: File) => {
|
||||
const payload = await fileToBase64Payload(file);
|
||||
return postJsonImageAction<FixArtistPortraitResult>(`${API}/artists/${id}/upload-portrait`, payload);
|
||||
},
|
||||
|
||||
updateArtistCheckupFlags: (
|
||||
id: number,
|
||||
flags: { checked?: boolean; fixed?: boolean }
|
||||
) =>
|
||||
fetch(`${API}/artists/${id}/checkup-flags`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(flags),
|
||||
}).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Update failed: ${res.status}`);
|
||||
}
|
||||
return res.json() as Promise<{ checked: boolean; fixed: boolean }>;
|
||||
}),
|
||||
};
|
||||
|
||||
export function debugImageProxyUrl(
|
||||
|
||||
Reference in New Issue
Block a user