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:
Danila Khodjaef
2026-06-21 13:14:06 +03:00
co-authored by Cursor
parent b4425445bb
commit 0972b5df99
56 changed files with 1980 additions and 92 deletions
+239 -2
View File
@@ -5,8 +5,8 @@ const fs = require('fs');
require('dotenv').config();
const pool = require('./db');
const { ensurePaintingImages, preloadArtistImagesLocal, replacePaintingImageFromUrl, IMAGE_DIR } = require('./image-service');
const { searchGoogleImagesFirst, fetchImageBuffer, friendlyImageFetchError, pickExt } = require('../scripts/image-fetcher');
const { ensurePaintingImages, preloadArtistImagesLocal, replacePaintingImageFromUrl, replaceArtistPortraitFromUrl, clearPaintingImage, clearArtistPortrait, replacePaintingImageFromBuffer, replaceArtistPortraitFromBuffer, IMAGE_DIR } = require('./image-service');
const { searchGoogleImagesFirst, searchArtistPortraitFirst, searchPaintingImagesMany, searchArtistPortraitMany, fetchImageBuffer, friendlyImageFetchError, pickExt } = require('../scripts/image-fetcher');
const app = express();
const PORT = process.env.PORT || 3001;
@@ -234,6 +234,173 @@ app.get('/api/artists/:id/navigation', async (req, res) => {
}
});
// Update artist portrait checkup flags (checked / fixed)
app.patch('/api/artists/:id/checkup-flags', async (req, res) => {
try {
const artistId = parseInt(req.params.id, 10);
const { checked, fixed } = req.body ?? {};
if (checked === undefined && fixed === undefined) {
return res.status(400).json({ error: 'Provide checked and/or fixed boolean' });
}
if (checked !== undefined && typeof checked !== 'boolean') {
return res.status(400).json({ error: 'checked must be a boolean' });
}
if (fixed !== undefined && typeof fixed !== 'boolean') {
return res.status(400).json({ error: 'fixed must be a boolean' });
}
const current = await pool.query(
`SELECT checkup_checked, checkup_fixed FROM artists WHERE id = $1`,
[artistId]
);
if (current.rows.length === 0) {
return res.status(404).json({ error: 'Artist not found' });
}
const willBeFixed = fixed !== undefined ? fixed : !!current.rows[0].checkup_fixed;
let nextChecked = checked;
if (willBeFixed) {
nextChecked = true;
}
const sets = [];
const params = [];
if (fixed !== undefined) {
params.push(fixed);
sets.push(`checkup_fixed = $${params.length}`);
}
if (nextChecked !== undefined) {
params.push(nextChecked);
sets.push(`checkup_checked = $${params.length}`);
}
params.push(artistId);
const result = await pool.query(
`UPDATE artists SET ${sets.join(', ')}
WHERE id = $${params.length}
RETURNING checkup_checked AS checked, checkup_fixed AS fixed`,
params
);
res.json({
checked: !!result.rows[0].checked,
fixed: !!result.rows[0].fixed,
});
} catch (err) {
console.error('Artist checkup flags error:', err.message);
res.status(500).json({ error: 'Failed to update checkup flags' });
}
});
// Developer debug: portrait image search for artist bio
app.get('/api/artists/:id/debug-portrait-search/more', async (req, res) => {
try {
const artistId = parseInt(req.params.id, 10);
const limit = Math.min(20, Math.max(1, parseInt(req.query.limit, 10) || 20));
const result = await pool.query(`SELECT name FROM artists WHERE id = $1`, [artistId]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Artist not found' });
}
const { name } = result.rows[0];
const search = await searchArtistPortraitMany(name, limit);
res.json(search);
} catch (err) {
console.error('Debug portrait search (more) error:', err.message);
res.status(500).json({ error: 'Portrait search failed' });
}
});
app.get('/api/artists/:id/debug-portrait-search', async (req, res) => {
try {
const artistId = parseInt(req.params.id, 10);
const result = await pool.query(`SELECT name FROM artists WHERE id = $1`, [artistId]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Artist not found' });
}
const { name } = result.rows[0];
const search = await searchArtistPortraitFirst(name);
res.json(search);
} catch (err) {
console.error('Debug portrait search error:', err.message);
res.status(500).json({ error: 'Portrait search failed' });
}
});
// Developer debug: replace artist portrait with a search result URL
app.post('/api/artists/:id/fix-portrait', async (req, res) => {
try {
const artistId = parseInt(req.params.id, 10);
const { imageUrl, searchUrl, source, pageUrl, thumbUrl } = req.body ?? {};
if (!imageUrl || typeof imageUrl !== 'string' || !/^https?:\/\//i.test(imageUrl)) {
return res.status(400).json({ error: 'Valid imageUrl required' });
}
const updated = await replaceArtistPortraitFromUrl(artistId, imageUrl, {
searchUrl: typeof searchUrl === 'string' ? searchUrl : undefined,
source: typeof source === 'string' ? source : undefined,
pageUrl: typeof pageUrl === 'string' ? pageUrl : undefined,
thumbUrl: typeof thumbUrl === 'string' ? thumbUrl : undefined,
});
await pool.query(
`UPDATE artists SET checkup_fixed = true, checkup_checked = true WHERE id = $1`,
[artistId]
);
res.json({ ...updated, fixed: true, checked: true });
} catch (err) {
console.error('Fix portrait error:', err.message);
res.status(500).json({ error: friendlyImageFetchError(err) });
}
});
app.post('/api/artists/:id/clear-portrait', async (req, res) => {
try {
const artistId = parseInt(req.params.id, 10);
const updated = await clearArtistPortrait(artistId);
await pool.query(
`UPDATE artists SET checkup_fixed = true, checkup_checked = true WHERE id = $1`,
[artistId]
);
res.json({ ...updated, fixed: true, checked: true });
} catch (err) {
console.error('Clear portrait error:', err.message);
res.status(500).json({ error: err.message || 'Could not clear portrait' });
}
});
app.post('/api/artists/:id/upload-portrait', async (req, res) => {
try {
const artistId = parseInt(req.params.id, 10);
const { imageData, mimeType } = req.body ?? {};
if (!imageData || typeof imageData !== 'string') {
return res.status(400).json({ error: 'imageData required' });
}
const buffer = Buffer.from(imageData, 'base64');
if (!buffer.length) {
return res.status(400).json({ error: 'Empty image data' });
}
if (buffer.length > 15 * 1024 * 1024) {
return res.status(400).json({ error: 'Image too large (max 15 MB)' });
}
const updated = await replaceArtistPortraitFromBuffer(
artistId,
buffer,
typeof mimeType === 'string' ? mimeType : 'image/jpeg'
);
await pool.query(
`UPDATE artists SET checkup_fixed = true, checkup_checked = true WHERE id = $1`,
[artistId]
);
res.json({ ...updated, fixed: true, checked: true });
} catch (err) {
console.error('Upload portrait error:', err.message);
res.status(500).json({ error: err.message || 'Could not upload portrait' });
}
});
// Artist detail with periods and paintings
app.get('/api/artists/:id', async (req, res) => {
try {
@@ -452,6 +619,30 @@ app.post('/api/artists/:id/preload-images', async (req, res) => {
});
// Developer debug: Google Images first result for image audit
app.get('/api/paintings/:id/debug-image-search/more', async (req, res) => {
try {
const paintingId = parseInt(req.params.id, 10);
const limit = Math.min(20, Math.max(1, parseInt(req.query.limit, 10) || 20));
const result = await pool.query(
`SELECT p.title, a.name AS artist_name
FROM paintings p
JOIN artists a ON a.id = p.artist_id
WHERE p.id = $1`,
[paintingId]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Painting not found' });
}
const { title, artist_name: artistName } = result.rows[0];
const search = await searchPaintingImagesMany(artistName, title, limit);
res.json(search);
} catch (err) {
console.error('Debug image search (more) error:', err.message);
res.status(500).json({ error: 'Image search failed' });
}
});
app.get('/api/paintings/:id/debug-image-search', async (req, res) => {
try {
const paintingId = parseInt(req.params.id, 10);
@@ -501,6 +692,52 @@ app.post('/api/paintings/:id/fix-image', async (req, res) => {
}
});
app.post('/api/paintings/:id/clear-image', async (req, res) => {
try {
const paintingId = parseInt(req.params.id, 10);
const updated = await clearPaintingImage(paintingId);
await pool.query(
`UPDATE paintings SET checkup_fixed = true, checkup_checked = true WHERE id = $1`,
[paintingId]
);
res.json({ ...updated, fixed: true, checked: true });
} catch (err) {
console.error('Clear image error:', err.message);
res.status(500).json({ error: err.message || 'Could not clear image' });
}
});
app.post('/api/paintings/:id/upload-image', async (req, res) => {
try {
const paintingId = parseInt(req.params.id, 10);
const { imageData, mimeType } = req.body ?? {};
if (!imageData || typeof imageData !== 'string') {
return res.status(400).json({ error: 'imageData required' });
}
const buffer = Buffer.from(imageData, 'base64');
if (!buffer.length) {
return res.status(400).json({ error: 'Empty image data' });
}
if (buffer.length > 15 * 1024 * 1024) {
return res.status(400).json({ error: 'Image too large (max 15 MB)' });
}
const updated = await replacePaintingImageFromBuffer(
paintingId,
buffer,
typeof mimeType === 'string' ? mimeType : 'image/jpeg'
);
await pool.query(
`UPDATE paintings SET checkup_fixed = true, checkup_checked = true WHERE id = $1`,
[paintingId]
);
res.json({ ...updated, fixed: true, checked: true });
} catch (err) {
console.error('Upload image error:', err.message);
res.status(500).json({ error: err.message || 'Could not upload image' });
}
});
// Proxy remote image for debug preview (avoids hotlink / CORS blocks)
app.get('/api/debug/image-proxy', async (req, res) => {
try {