Files
Art-gallery/Documentation/API.md
T
Danila KhodjaefandCursor 48bd17e985 Add curator Influences tool with import wizard, CRUD, and graph.
CSV/JSON/XLSX mapping wizard expands artist-level rows to all paintings, blocks duplicate file/data imports via content and payload hashes, and documents the workflow in influence-import.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 18:54:26 +03:00

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

Caching: /images responses use Cache-Control: public, max-age=0, must-revalidate with ETag / Last-Modified. Painting and artist JSON payloads include optional image_cache_key / thumbnail_cache_key (and portrait_cache_key / portrait_thumb_cache_key on artists) — Unix ms from the files mtime on disk. The client appends ?v=<key> to image URLs so fix/upload/clear updates show immediately after reload even when the relative path is unchanged.

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.


Locale (?locale=ru)

Public catalog endpoints accept optional locale query param (en default, ru supported) or Accept-Language: ru.

Affected routes: /api/catalog/bootstrap, /api/timeline, /api/search, /api/artists, /api/artists/:id, /api/paintings/:id, /api/movements/:id/gallery, /api/movements/:id/artists, /api/artists/:id/navigation.

Responses include "locale": "ru" when resolved. Display field names are unchanged; values come from entity_translations when status = published, else canonical English.

Full guide: i18n-russian.md.


Translations (curator)

Requires curator session. Base path: /api/translations.

Method Path Purpose
GET /api/translations/coverage?locale=ru Coverage stats
GET /api/translations/worklist/:entityType?locale=ru Artists/paintings/movements with translation status
GET /api/translations/:entityType/:id Canonical + all translation rows
PUT /api/translations/:entityType/:id Upsert fields { locale, fields, status }
POST /api/translations/:entityType/:id/publish Publish all draft/reviewed rows for locale

Influences (curator)

Requires curator session. Base path: /api/influences. See influence-import.md.

Method Path Purpose
GET /api/influences List edges (artistId, paintingId, q, pagination)
GET /api/influences/graph?artistId= Neighborhood nodes/edges for visualization
POST /api/influences Create edge { paintingId, sourceType, sourceArtistId|sourcePaintingId|sourceMovementId, … }
PATCH /api/influences/:id Update notes / remap source
DELETE /api/influences/:id Delete edge
POST /api/influences/import/parse Parse upload { filename, contentBase64, sheet? } — returns contentHash / payloadHash / alreadyImported
POST /api/influences/import/preview Validate { rows, mapping, contentHash?, payloadHash? }
POST /api/influences/import/commit Insert { proposals, contentHash?, payloadHash?, force? }409 if duplicate unless force

Public painting detail still exposes read-only influencedBy / influenced (unchanged).


GET /api/search

Public catalog search over artists, paintings, and art movements. Used by the timeline header search bar (CatalogSearchBar.tsx).

Query

Param Type Default Description
q string Search text (min 2 characters after trim; shorter returns { q, results: [] })
limit int 20 Max results total (capped at 50)
types string all Optional comma list: artist, painting, movement
locale string en ru — search and return published Russian aliases when available

Matching (case-insensitive ILIKE):

Entity Fields
Artist name, wikipedia_title, movement name
Movement movement name, era name
Painting title, wikipedia_title, year (as text), artist name, movement name
All (when locale=ru) Published rows in entity_translations for name / title

Prefix matches on primary labels (name / title) rank before substring matches.

Response

{
  "q": "monet",
  "results": [
    {
      "type": "artist",
      "id": 19,
      "name": "Claude Monet",
      "birth_year": 1840,
      "death_year": 1926,
      "movement_name": "Impressionism",
      "portrait_path": "portraits/Claude_Monet.jpg",
      "portrait_thumb_path": "portraits/thumbs/Claude_Monet_thumb.jpg"
    },
    {
      "type": "movement",
      "id": 12,
      "name": "Impressionism",
      "color": "#87CEEB",
      "start_year": 1860,
      "end_year": 1890,
      "era_name": "Modern"
    },
    {
      "type": "painting",
      "id": 241,
      "title": "Water Lilies",
      "year": 1919,
      "artist_id": 19,
      "artist_name": "Claude Monet",
      "movement_name": "Impressionism",
      "thumbnail_path": "paintings/thumbs/Claude_Monet_Water_Lilies_thumb.jpg",
      "image_path": "paintings/Claude_Monet_Water_Lilies.jpg"
    }
  ]
}

Indexes: applied by npm run dev:migrate (db/migrate-search.sql) or standalone npm run dev:migrate:search.

Client: api.search(q, { limit?, types? }).

Navigation from search: choosing a painting opens detail with returnTo: timeline; the client shows ← Back to Timeline and calls goToTimelineHome() (clears gallery session, resets timeline zoom). Choosing an artist or movement uses the normal gallery entry handlers.


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": "paintings/Artist_Title.jpg",
    "thumbnail_path": "paintings/thumbs/Artist_Title_thumb.jpg",
    "image_cache_key": 1739123456789,
    "thumbnail_cache_key": 1739123456790,
    "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",
  "image_cache_key": 1739123456789,
  "thumbnail_cache_key": 1739123456790,
  "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, image_cache_key, thumbnail_cache_key, 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.search(q, options?) GET /api/search
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, revision?) /images/<path> or placeholder; optional ?v= cache buster
paintingImageRevision(painting, sessionRevision?) Prefer API image_cache_key / thumbnail_cache_key, else in-session counter
galleryImageUrl(painting, sessionRevision?) Local thumb/full only (3D); auto ?v= from cache keys
galleryImageUrlWithRevision(painting, sessionRevision?) Alias of galleryImageUrl
paintingImageUrl(painting, sessionRevision?) Local file, on-demand API, or null when cleared (checkup_fixed + no paths)
portraitUrl(path, revision?, artist?) /images/<path> with ?v= from revision or artist cache keys
validateDebugUploadFile(file) Client-side size/type check before base64 upload
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=…