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>
14 KiB
Art Gallery — REST API
Base URL in development:
- Direct:
http://localhost:3001 - Via Vite proxy:
http://localhost:5173(same paths)
All JSON responses use Content-Type: application/json. Errors return { "error": "message" } with an appropriate HTTP status.
Static images are served at /images/<relative-path> from IMAGE_DIR.
GET /api/bounds
Returns the overall timeline year range used to initialise the zoomable timeline.
Response
{
"min_year": -800,
"max_year": 2100
}
min_year is the earliest art movement start; max_year is the latest of era ends, movement ends, and artist death years.
GET /api/timeline
Historical eras and art movements overlapping a year window.
Query
| Param | Type | Default | Description |
|---|---|---|---|
start |
int | -3000 | Window start year |
end |
int | 2100 | Window end year |
Response
{
"eras": [ { "id": 1, "name": "Renaissance", "start_year": 1300, "end_year": 1600, "start_definite": false, "end_definite": false, "description": "...", "sort_order": 3 } ],
"movements": [ { "id": 5, "name": "Impressionism", "start_year": 1860, "end_year": 1890, "era_id": 6, "era_name": "Modern", "color": "#87CEEB", ... } ]
}
Movements are filtered to those with at least one artist active in the requested range.
GET /api/artists
Artists for timeline portraits and the movement flow diagram.
Query
| Param | Type | Description |
|---|---|---|
start |
int | Only artists alive after this year |
end |
int | Only artists born before this year |
movement_id |
int | Filter by movement |
Response — array of artist objects with joined movement_name and movement_color.
GET /api/movements/:id/artists
Artists belonging to a single movement.
Response — array of { id, name, birth_year, death_year, portrait_path, bio_short }.
GET /api/artists/:id
Full artist profile for the bio page and 3D gallery entry.
Response
{
"artist": {
"id": 1,
"name": "...",
"birth_year": 1840,
"death_year": 1926,
"movement_name": "...",
"portrait_path": "portraits/Claude_Monet.jpg",
"bio_short": "First two sentences from Wikipedia…",
"bio_full": "Full Wikipedia lead section…",
"wikipedia_title": "Claude Monet",
"checkup_checked": false,
"checkup_fixed": false
},
"periods": [ { "id": 1, "name": "Milan Period", "start_year": 1482, "end_year": 1499, ... } ],
"paintings": [ { "id": 10, "title": "...", "year": 1498, "image_path": "...", "thumbnail_path": "...", "wikipedia_title": "...", "has_influence_links": true, "checkup_checked": false, "checkup_fixed": false, ... } ]
}
Each painting includes:
| Field | Meaning |
|---|---|
has_influence_links |
true when the work appears in any influence row — 3D gallery shows a golden lamp above the frame |
checkup_checked |
Reviewed in checkup / debug workflow (gold frame in 3D when true) |
checkup_fixed |
Image replaced via Fix it |
Populate biographies with npm run fetch-artist-bios (see data-and-images.md).
Artist objects also include checkup_checked and checkup_fixed (same semantics as paintings; gold portrait border when reviewed). Run npm run migrate:artist-checkup-flags on existing databases.
PATCH /api/artists/:id/checkup-flags
Update artist portrait review flags. Body: { "checked"?: boolean, "fixed"?: boolean } — at least one field required.
Same rules as painting checkup flags: setting fixed: true also sets checked: true.
Response
{ "checked": true, "fixed": false }
GET /api/artists/:id/debug-portrait-search
Portrait image search for debug mode on the artist bio page (Custom Search → Google Arts & Culture → scrape → DuckDuckGo).
Response — same shape as painting debug search (query, imageUrl, searchUrl, source, optional thumbUrl, sourceLabel).
GET /api/artists/:id/debug-portrait-search/more
Up to 20 ranked portrait candidates for the More picker modal.
Query: limit (int, default 20, max 20)
Response
{
"query": "Leonardo da Vinci portrait",
"searchUrl": "https://…",
"source": "google-custom-search",
"results": [
{ "imageUrl": "https://…", "thumbUrl": "https://…", "source": "google-custom-search" }
]
}
POST /api/artists/:id/fix-portrait
Download a remote URL and replace the artist’s local portrait. Sets checkup_fixed = true and checkup_checked = true.
Body — same as POST /api/paintings/:id/fix-image (imageUrl required; optional searchUrl, source, thumbUrl).
Response
{
"portraitPath": "portraits/Leonardo_da_Vinci.jpg",
"fixed": true,
"checked": true
}
POST /api/artists/:id/clear-portrait
Delete the portrait file from disk, set portrait_path = NULL, and set both checkup flags. Used by debug Clear; the bio page shows an empty portrait slot (no placeholder).
Response
{
"portraitPath": null,
"fixed": true,
"checked": true
}
POST /api/artists/:id/upload-portrait
Upload a local image (base64 JSON body). Validates with sharp, resizes to portrait dimensions, sets checkup flags.
Body
{
"imageData": "<base64>",
"mimeType": "image/jpeg"
}
Max size 15 MB. Response — same as fix-portrait.
GET /api/artists/:id/navigation
Related artists for hall-to-hall navigation at the exit doorway. Derived from painting_influences:
- Predecessors — artists whose works influenced this artist’s paintings.
- Successors — artists whose works were influenced by this artist’s paintings.
Both lists are grouped by art movement and exclude the current artist.
Response
{
"predecessors": [
{
"movement_id": 4,
"movement_name": "Early Renaissance",
"movement_color": "#B8860B",
"artists": [
{
"id": 12,
"name": "Domenico Ghirlandaio",
"birth_year": 1448,
"death_year": 1494,
"portrait_path": "portraits/Domenico_Ghirlandaio.jpg"
}
]
}
],
"successors": []
}
POST /api/artists/:id/preload-images
Fast local scan: links paintings to files already on disk. Does not download from the internet (safe to call before opening the 3D gallery).
Response
{ "fetched": 12, "total": 15 }
fetched counts paintings with a resolvable local image after the scan.
GET /api/paintings/:id
Painting detail with influence graph neighbours.
Response
{
"painting": { "id": 10, "title": "...", "artist_name": "...", "image_path": "...", "checkup_checked": false, "checkup_fixed": false, "has_influence_links": true, ... },
"influencedBy": [
{
"source_type": "painting",
"id": 3, "title": "...", "artist_name": "...", "year": 1885,
"notes": "...", "aspects": "...", "quote": "...", "source_author": "...", "source_url": "..."
},
{
"source_type": "artist",
"source_artist_id": 12, "source_artist_name": "Paul Cézanne",
"artist_portrait": "...", "period_note": "during creation (1907)",
"period_start_year": 1907, "period_end_year": 1907
},
{
"source_type": "movement",
"movement_id": 8, "movement_name": "Fauvism", "movement_color": "#c45c26",
"period_start_year": 1905, "period_end_year": 1907
}
],
"influenced": [ { "source_type": "painting", "id": 20, "title": "...", ... } ]
}
painting.has_influence_links matches the flag on GET /api/artists/:id paintings (see above).
GET /api/paintings/:id/image
Serves a painting image file. Resolves and caches from external sources only when no local file exists.
Query
| Param | Values | Default |
|---|---|---|
size |
thumb, full |
thumb |
Returns the image bytes with Cache-Control: public, max-age=86400, or 404 if unavailable.
Developer image audit
Routes for the Checkup page and Debug mode on painting detail and artist bio. Register GET /api/paintings/checkup before GET /api/paintings/:id so "checkup" is not parsed as a painting id.
GET /api/paintings/checkup
Full-catalog audit table for the Checkup UI.
Response
{
"paintings": [
{
"id": 36,
"title": "Trinity",
"artist_name": "Andrei Rublev",
"year": 1411,
"gallery_file": "paintings/Andrei_Rublev_Trinity.jpg",
"gallery_preview": "paintings/thumbs/Andrei_Rublev_Trinity_thumb.jpg",
"detail_file": "paintings/Andrei_Rublev_Trinity.jpg",
"detail_preview": "paintings/thumbs/Andrei_Rublev_Trinity_thumb.jpg",
"detail_on_demand": false,
"gallery_file_exists": true,
"detail_file_exists": true,
"checked": false,
"fixed": false
}
],
"total": 1201
}
| Field | Meaning |
|---|---|
checked |
Reviewed in checkup workflow (checkup_checked) |
fixed |
Image replaced via Fix (checkup_fixed) |
detail_on_demand |
true when detail would use GET /api/paintings/:id/image?size=full |
gallery_file_exists / detail_file_exists |
Disk check under IMAGE_DIR |
PATCH /api/paintings/:id/checkup-flags
Update review flags. Body: { "checked"?: boolean, "fixed"?: boolean } — at least one field required.
Setting fixed: true also sets checked: true. While fixed is true, the API rejects clearing checked.
Response
{ "checked": true, "fixed": false }
GET /api/paintings/:id/debug-image-search
Google-family image search for debug / checkup (Custom Search → Google Arts & Culture → scrape → DuckDuckGo). Used by Search visible on the Checkup page and the debug panel on painting detail.
Response
{
"query": "Andrei Rublev Trinity painting",
"imageUrl": "https://…",
"source": "google-arts",
"pageUrl": "https://…"
}
POST /api/paintings/:id/fix-image
Download a remote URL and replace the painting’s local full image + thumbnail. Sets checkup_fixed = true and checkup_checked = true.
Uses downloadImageForFix in scripts/image-fetcher.js (browser User-Agent, referer fallbacks, Wikimedia URL upgrades) for reliable downloads from Google Arts, Commons, etc.
Body
{
"imageUrl": "https://…",
"searchUrl": "https://…",
"source": "google-arts",
"thumbUrl": "https://…"
}
Only imageUrl is required; optional fields improve fetch success for hotlinked URLs.
Response
{
"imagePath": "paintings/Artist_Title.jpg",
"thumbnailPath": "paintings/thumbs/Artist_Title_thumb.jpg",
"fixed": true,
"checked": true
}
GET /api/paintings/:id/debug-image-search/more
Up to 20 ranked painting image candidates for the More picker modal.
Query: limit (int, default 20, max 20)
Response
{
"query": "Andrei Rublev Trinity painting",
"searchUrl": "https://…",
"source": "google-arts",
"results": [
{ "imageUrl": "https://…", "thumbUrl": "https://…", "source": "google-arts" }
]
}
POST /api/paintings/:id/clear-image
Delete full + thumbnail files from disk, set image_path and thumbnail_path to NULL, and set both checkup flags. Used by debug Clear; detail view shows an empty frame (no placeholder, no on-demand refetch).
Response
{
"imagePath": null,
"thumbnailPath": null,
"fixed": true,
"checked": true
}
POST /api/paintings/:id/upload-image
Upload a local painting image (base64 JSON body). Validates with sharp, writes full file, regenerates thumbnail, sets checkup flags.
Body
{
"imageData": "<base64>",
"mimeType": "image/jpeg"
}
Max size 15 MB. Response — same shape as fix-image (imagePath, thumbnailPath, fixed, checked).
GET /api/debug/image-proxy
Proxy a remote image URL for debug preview (avoids hotlink / CORS blocks in the browser).
Query: url — must be http:// or https://
Returns image bytes with appropriate Content-Type.
Frontend helpers
The React client wraps these endpoints in client/src/api/client.ts:
| Function | Maps to |
|---|---|
api.getBounds() |
GET /api/bounds |
api.getTimeline(start, end) |
GET /api/timeline |
api.getArtists(...) |
GET /api/artists |
api.getArtist(id) |
GET /api/artists/:id |
api.getArtistNavigation(id) |
GET /api/artists/:id/navigation |
api.getPainting(id) |
GET /api/paintings/:id |
preloadArtistImages(id) |
POST /api/artists/:id/preload-images |
imageUrl(path) |
/images/<path> or placeholder |
galleryImageUrl(painting) |
Local thumb/full only (3D) |
galleryImageUrlWithRevision(painting, revision) |
Local URL with ?v= cache buster after fix |
paintingImageUrl(painting) |
Local file, on-demand API, or null when cleared (checkup_fixed + no paths) |
portraitUrl(path, revision?) |
/images/<path> with optional ?v= cache buster |
api.getPaintingCheckup() |
GET /api/paintings/checkup |
api.updatePaintingCheckupFlags(id, flags) |
PATCH /api/paintings/:id/checkup-flags |
api.getPaintingDebugImageSearch(id) |
GET /api/paintings/:id/debug-image-search |
api.getPaintingDebugImageSearchMore(id, limit?) |
GET /api/paintings/:id/debug-image-search/more |
api.fixPaintingImage(id, imageUrl, context?) |
POST /api/paintings/:id/fix-image |
api.clearPaintingImage(id) |
POST /api/paintings/:id/clear-image |
api.uploadPaintingImage(id, file) |
POST /api/paintings/:id/upload-image |
api.updateArtistCheckupFlags(id, flags) |
PATCH /api/artists/:id/checkup-flags |
api.getArtistDebugPortraitSearch(id) |
GET /api/artists/:id/debug-portrait-search |
api.getArtistDebugPortraitSearchMore(id, limit?) |
GET /api/artists/:id/debug-portrait-search/more |
api.fixArtistPortrait(id, imageUrl, context?) |
POST /api/artists/:id/fix-portrait |
api.clearArtistPortrait(id) |
POST /api/artists/:id/clear-portrait |
api.uploadArtistPortrait(id, file) |
POST /api/artists/:id/upload-portrait |
debugImageProxyUrl(imageUrl, context?) |
GET /api/debug/image-proxy?url=… |