Files
Art-gallery/Documentation/API.md
T
Danila KhodjaefandCursor a2263ca186 Document gallery loading markers, WebGL recovery, and movement filtering.
Update basics.md for the new loading indicators, 3D WebGL context-loss recovery, and the corrected movement-visibility rule (span overlap plus any catalogued artist). Refresh remaining docs and config for the current dev-first workflow and npm script names.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 12:36:34 +03:00

23 KiB
Raw Blame History

Art Gallery — REST API

Base URL (paths are the same on every host; only the origin changes):

Context Base URL
Production (public) https://gallery.mysuperlab.netcraze.pro
Production (LAN) http://192.168.10.122:5173
Development (public) https://devgallery.mysuperlab.netcraze.pro
Development (LAN) http://192.168.10.70:5173
Local Vite proxy http://localhost:5173 (proxies /api and /images to API on :3451)
Local API only http://localhost:3451 (when using npm run dev:web)

See environments.md for Keenetic rules, databases, and deploy.

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.

Quick check:

curl.exe -sk https://gallery.mysuperlab.netcraze.pro/api/bounds
curl.exe -sk https://devgallery.mysuperlab.netcraze.pro/api/bounds

Authentication

Anonymous visitors have implicit role user (browse only). Curator accounts unlock debug mode, the Checkup page, and all mutating audit routes.

Sessions use an HTTP-only cookie (gallery.sid). The client sends credentials: 'include' on API requests.

GET /api/auth/me

Response (anonymous)

{ "role": "user" }

Response (curator session)

{ "role": "curator", "username": "curator" }

POST /api/auth/login

Body: { "username": "curator", "password": "…" }

Response: { "role": "curator", "username": "curator" }

Errors: 401 invalid credentials, 400 missing fields.

POST /api/auth/logout

Destroys the session cookie.

Response: { "ok": true }

Curator-only routes

These return 401 with { "error": "Curator login required" } without a valid curator session:

Route Audit action (mutations only)
GET /api/paintings/checkup — (read)
GET /api/paintings/:id/debug-image-search (+ /more)
GET /api/artists/:id/debug-portrait-search (+ /more)
GET /api/debug/image-proxy
PATCH /api/paintings/:id/checkup-flags painting.checkup_flags
PATCH /api/artists/:id/checkup-flags artist.checkup_flags
POST /api/paintings/:id/fix-image painting.fix_image
POST /api/paintings/:id/clear-image painting.clear_image
POST /api/paintings/:id/upload-image painting.upload_image
DELETE /api/paintings/:id painting.delete
POST /api/artists/:id/fix-portrait artist.fix_portrait
POST /api/artists/:id/clear-portrait artist.clear_portrait
POST /api/artists/:id/upload-portrait artist.upload_portrait

Public (no login): all catalog GET routes, POST /api/artists/:id/preload-images (local file linking for 3D halls), /images, SPA static.

Curator mutations are recorded in curator_audit_log (see DB_structure.md).


GET /api/catalog/bootstrap

Preferred for timeline first paint. Returns bounds, eras, movements, and slim artist rows in a single response (replaces the separate bounds + timeline + artists?timeline=1 waterfall).

Query

Param Type Default Description
start int bounds min_year Window start year
end int bounds max_year Window end year

Response

{
  "bounds": { "min_year": -800, "max_year": 2100 },
  "eras": [  ],
  "movements": [  ],
  "artists": [
    {
      "id": 1,
      "name": "Claude Monet",
      "birth_year": 1840,
      "death_year": 1926,
      "movement_id": 12,
      "portrait_path": "portraits/Claude_Monet.jpg",
      "portrait_thumb_path": "portraits/thumbs/Claude_Monet_thumb.jpg",
      "wikipedia_title": "Claude Monet",
      "century": 19,
      "movement_name": "Impressionism",
      "movement_color": "#6B8E9F"
    }
  ]
}

Caching: Cache-Control: public, max-age=300 with ETag (304 when catalog row counts unchanged).

The React home page loads this endpoint once on mount. Pan and zoom filter movements and portraits client-side — no refetch per view change.


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
timeline bool When 1 or true, return a lightweight row set for the home-page timeline (omits bio_short, bio_full; includes portrait_thumb_path)

Response — array of artist objects with joined movement_name and movement_color.

The React home page loads the timeline catalog once on mount via GET /api/catalog/bootstrap (or legacy: GET /api/bounds + GET /api/timeline + GET /api/artists?timeline=1). Pan and zoom filter movements and portraits client-side — no refetch per view change. Rapid pan/zoom is batched with createViewChangeScheduler() (one React update per animation frame).


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,
    "palette_metadata": { "nationality": "French", "styles": "Impressionism, …", "source": "PainterPalette.csv", ... }
  },
  "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 dev: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). After npm run dev:import-painter-palette, palette_metadata holds PainterPalette enrichment (nationality, styles, occupations, raw influence fields, etc.). Run npm run dev:migrate:artist-checkup-flags and npm run dev:migrate:artist-palette 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_influence_sources:

  • Predecessors — artists whose works (or artist records) influenced this artists paintings.
  • Successors — artists whose paintings were influenced by this artists works (via painting sources), or whose paintings cite this artist as a source (via artist sources).

Both lists are grouped by art movement and exclude the current artist. Each artist appears once even when multiple influence edges exist.

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

Public — no curator login required.

Fast local scan: links paintings to files already on disk. Does not download from the internet. The 3D client calls this automatically when entering an artist hall.

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).

Both influencedBy and influenced are read from painting_influence_sources only (one row per edge). Painting-type sources appear in Influenced By when they are predecessors; in Influenced when this painting is the source of a later work. Artist and movement sources appear only in Influenced By. The legacy painting_influences table is not merged into these responses — it mirrors painting edges for scripts only.

In the UI, side-panel thumbnails use the paintings local image with letterboxing (object-fit: contain) so the full composition is visible. Click a thumbnail to open that works detail view.


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

Curator login required for every route in this section. See Authentication above.

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).


DELETE /api/paintings/:id

Permanently remove a painting (debug Remove entry on painting detail). Deletes full + thumbnail files from disk, then deletes the paintings row. Related rows in painting_influence_sources, painting_annotations, and legacy painting_influences are removed by ON DELETE CASCADE.

Response

{
  "id": 42,
  "artistId": 7,
  "title": "Example Work"
}

Errors: 404 if the painting does not exist.

The client refetches the artist (and movement gallery when applicable), remounts the 3D hall, and opens the next or previous work in the catalog — or returns to the gallery if it was the last work.


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. All requests send credentials: 'include' for session cookies.

Function Maps to
getAuthMe() GET /api/auth/me
loginCurator(user, pass) POST /api/auth/login
logoutCurator() POST /api/auth/logout
api.getBounds() GET /api/bounds
api.getTimeline(start, end) GET /api/timeline
api.getArtists(...) GET /api/artists
api.getTimelineArtists() GET /api/artists?timeline=1
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.deletePainting(id) DELETE /api/paintings/:id
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=…