Files
Art-gallery/Documentation/API.md
T
Danila KhodjaefandCursor df29848d89 Add movement gallery wings with period interiors and expand timeline features.
Movement galleries split large catalogs into chronological wings (~55 works), use era-themed 3D interiors with side-wall windows, wing navigator on the back exit, and front archways between wings. Also adds painting annotations, timeline event guides, portrait hover highlights, and documentation/API updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 16:54:19 +03:00

16 KiB
Raw Blame History

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/movements/:id/gallery

Full movement gallery payload for the 3D movement wings view (opened by clicking a movement name on the home page).

Response

{
  "movement": {
    "id": 34,
    "name": "Baroque",
    "start_year": 1600,
    "end_year": 1750,
    "era_id": 6,
    "era_name": "Baroque",
    "color": "#8B4513",
    ...
  },
  "paintings": [
    {
      "id": 120,
      "title": "...",
      "year": 1640,
      "artist_id": 5,
      "artist_name": "Rembrandt",
      "image_path": "paintings/...",
      "thumbnail_path": "paintings/thumbs/...",
      "checkup_checked": false,
      "checkup_fixed": false,
      "has_influence_links": true,
      ...
    }
  ]
}
Field Meaning
movement.era_name Joined from historical_eras — used to pick period interior styling
paintings[].artist_name Artist display name for frame captions (year · artist)
paintings order Chronological: year, then sort_order, artist birth year, title

Includes all paintings whose artist.movement_id matches :id. Returns 404 if the movement does not exist.

Client: api.getMovementGallery(id) in client/src/api/client.ts; rendered by VirtualGallery in mode: 'movement'.


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 }

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", "width": 1200, "height": 1600 }
  ]
}

Each result may include optional width and height (pixels). The More modal shows these under each thumbnail; when missing, the client probes dimensions via GET /api/debug/image-proxy.


POST /api/artists/:id/fix-portrait

Download a remote URL and replace the artists 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 decoded size 15 MB (JSON body limit 20 MB on the server). 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 artists paintings.
  • Successors — artists whose works were influenced by this artists 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": "...", ... } ],
  "annotations": [
    {
      "id": 1,
      "label": "Central figure",
      "body": "Short art-history note…",
      "category": "subject",
      "pos_x": 42.5,
      "pos_y": 38.0,
      "source_author": "E.H. Gombrich",
      "source": "The Story of Art",
      "source_url": "https://…",
      "sort_order": 0,
      "confidence": "curated"
    }
  ]
}

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 }

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 paintings 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", "width": 2400, "height": 1800 }
  ]
}

Optional width / height on each result — see portrait more endpoint above.


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 decoded size 15 MB (JSON body limit 20 MB on the server). 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=…