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
+166 -3
View File
@@ -93,7 +93,9 @@ Full artist profile for the bio page and 3D gallery entry.
"portrait_path": "portraits/Claude_Monet.jpg",
"bio_short": "First two sentences from Wikipedia…",
"bio_full": "Full Wikipedia lead section…",
"wikipedia_title": "Claude Monet"
"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, ... } ]
@@ -110,6 +112,102 @@ Each painting includes:
Populate biographies with `npm run fetch-artist-bios` (see [data-and-images.md](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**
```json
{ "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**
```json
{
"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 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**
```json
{
"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**
```json
{
"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**
```json
{
"imageData": "<base64>",
"mimeType": "image/jpeg"
}
```
Max size 15 MB. **Response** — same as `fix-portrait`.
---
## `GET /api/artists/:id/navigation`
@@ -212,7 +310,7 @@ Returns the image bytes with `Cache-Control: public, max-age=86400`, or `404` if
## Developer image audit
Routes for the **Checkup** page and **Debug mode** on painting detail. Register `GET /api/paintings/checkup` **before** `GET /api/paintings/:id` so `"checkup"` is not parsed as a painting id.
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`
@@ -315,6 +413,61 @@ Only `imageUrl` is required; optional fields improve fetch success for hotlinked
---
### `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**
```json
{
"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**
```json
{
"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**
```json
{
"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).
@@ -341,9 +494,19 @@ The React client wraps these endpoints in `client/src/api/client.ts`:
| `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 or on-demand API |
| `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=…` |
+10 -5
View File
@@ -66,10 +66,14 @@ Finer-grained styles (Impressionism, Cubism, Suprematism, …).
| `name` | VARCHAR(200) | |
| `birth_year`, `death_year` | INTEGER | Nullable; used for timeline portrait placement |
| `movement_id` | FK → `art_movements` | Primary movement |
| `portrait_path` | VARCHAR(500) | Relative to `data/images/` |
| `portrait_path` | VARCHAR(500) | Relative to `data/images/`; nullable after debug **Clear** |
| `bio_short`, `bio_full` | TEXT | Wikipedia lead section (`npm run fetch-artist-bios`) |
| `wikipedia_title` | VARCHAR(300) | Source page title |
| `century` | INTEGER | Rounded century bucket for seeding limits |
| `checkup_checked` | BOOLEAN NOT NULL DEFAULT false | Portrait reviewed in debug workflow (gold border on bio when true) |
| `checkup_fixed` | BOOLEAN NOT NULL DEFAULT false | Portrait replaced, cleared, or uploaded via debug |
Applied by `npm run migrate:artist-checkup-flags` (`db/migrate-artist-checkup-flags.sql`).
### `artist_periods`
@@ -94,14 +98,14 @@ Phases within an artists career (e.g. “Blue Period”, “Roman Period”).
| `title` | VARCHAR(300) | |
| `year`, `year_end` | INTEGER | Creation date(s) |
| `description` | TEXT | |
| `image_path` | VARCHAR(500) | Full-size local file |
| `thumbnail_path` | VARCHAR(500) | Smaller variant for lists / 3D |
| `image_path` | VARCHAR(500) | Full-size local file; nullable after debug **Clear** |
| `thumbnail_path` | VARCHAR(500) | Smaller variant for lists / 3D; nullable after **Clear** |
| `wikipedia_title` | VARCHAR(300) | Used by image fetcher |
| `sort_order` | INTEGER | |
| `checkup_checked` | BOOLEAN NOT NULL DEFAULT false | Reviewed in image checkup workflow (UI label: **Reviewed**) |
| `checkup_fixed` | BOOLEAN NOT NULL DEFAULT false | Image corrected via checkup / debug **Fix** |
| `checkup_fixed` | BOOLEAN NOT NULL DEFAULT false | Image corrected, cleared, or uploaded via checkup / debug |
When `checkup_fixed` is true, `checkup_checked` is set automatically and cannot be cleared until **Fixed** is off.
When `checkup_fixed` is true, `checkup_checked` is set automatically and cannot be cleared until **Fixed** is off. A cleared painting (`image_path` and `thumbnail_path` both null, `checkup_fixed` true) is shown as an empty frame in detail view and is not refetched on demand.
Applied by `npm run migrate:checkup-flags` (`db/migrate-checkup-flags.sql`).
@@ -157,6 +161,7 @@ Used by painting detail API (`influencedBy`). Painting-type rows also feed `has_
- `artists(movement_id)`, `artists(century)`
- `paintings(artist_id)`, `paintings(period_id)`, `paintings(checkup_checked)`, `paintings(checkup_fixed)`
- `artists(checkup_checked)`, `artists(checkup_fixed)`
- `art_movements(era_id)`, `art_movements(start_year, end_year)`
- `painting_influences(painting_id)`, `painting_influences(influenced_by_painting_id)`
- `painting_influence_sources(painting_id)`, `painting_influence_sources(source_artist_id)`, `painting_influence_sources(source_movement_id)`
+15 -10
View File
@@ -10,7 +10,7 @@ The app is organised as a **drill-down hierarchy**:
2. **Movement flow** — art movements as curved SVG streams on the same year axis; documented predecessor→successor branches; portrait thumbnails placed along each stream.
3. **3D gallery** — one personal hall per artist; paintings on the walls, open centre, single exit for influence-based navigation.
4. **Painting detail** — full work in the centre, *Influenced By* on the left, *Influenced* on the right (paintings, artists, or movements), prev/next catalog browsing, optional fullscreen, link to artist biography.
5. **Artist biography** — portrait, lifespan, movement, and Wikipedia-sourced intro text (`bio_short` / `bio_full`).
5. **Artist biography** — portrait, lifespan, movement, and Wikipedia-sourced intro text (`bio_short` / `bio_full`). With debug mode on, the same image-audit panel as painting detail (portrait search, **Checked** / **Fix it** / **More** / **Clear** / **Upload**).
All artwork images are stored locally under `data/images/` — the UI never hot-links to Wikipedia or Commons at runtime (except optional on-demand fetch when a file is missing).
@@ -33,6 +33,8 @@ Gallery/
│ ├── src/ # Source (components, pages, 3D scene)
│ │ ├── components/VirtualGallery.tsx # 3D hall (parquet, frames, museum exit)
│ │ ├── components/PaintingDetail.tsx # Detail view + debug panel
│ │ ├── components/ArtistBio.tsx # Biography + portrait debug panel
│ │ ├── components/DebugSearchResultsModal.tsx # “More” search picker (20 results)
│ │ ├── components/MovementBands.tsx # Movement flow (SVG streams + branches)
│ │ ├── pages/CheckupPage.tsx # Image audit table
│ │ ├── data/historical-events.ts # Timeline event markers (UI)
@@ -225,7 +227,7 @@ Opened from the 3D hall (click a frame) or from influence thumbnails on another
### Debug mode (developer)
When **Debug mode** is enabled from the home header, painting detail shows a bottom-left panel with image search preview and **Checked** / **Fix it** buttons. See [Developer tools (image audit)](#developer-tools-image-audit).
When **Debug mode** is enabled from the home header, painting detail and artist biography show a bottom-left panel with image search preview and five action buttons. See [Developer tools (image audit)](#developer-tools-image-audit).
## Key design decisions
@@ -243,20 +245,23 @@ Optional workflow for curating local image files — not part of the public visi
| Feature | Where | Purpose |
|---------|--------|---------|
| **Debug mode** | Home header toggle (`client/src/utils/debugMode.ts`) | Persists in `localStorage`; enables debug panel on painting detail |
| **Debug mode** | Home header toggle (`client/src/utils/debugMode.ts`) | Persists in `localStorage`; enables debug panel on painting detail and artist bio |
| **Checkup page** | Home header → **Checkup** (`CheckupPage.tsx`) | Full-catalog table: gallery vs detail thumbnails, search, fix, review flags |
| **Debug panel** | Painting detail (bottom-left, when debug mode on) | Search preview + two action buttons |
| **Debug panel** | Painting detail or artist bio (bottom-left, when debug mode on) | Search preview + five action buttons |
### Debug panel (painting detail)
### Debug panel (painting detail and artist bio)
When debug mode is on, a panel at the bottom-left shows the image search query, a preview when a result is found, and **two buttons**:
When debug mode is on, a panel at the bottom-left shows the image search query, a preview when a result is found, and **five buttons** in two rows:
| Button | Action |
|--------|--------|
| **Checked** | Sets `checkup_checked` via `PATCH /api/paintings/:id/checkup-flags` (disabled once already reviewed) |
| **Fix it** | Replaces local full + thumbnail from the search result via `POST /api/paintings/:id/fix-image`; sets **Fixed** and **Reviewed** |
| **Checked** | Sets `checkup_checked` via `PATCH /checkup-flags` (disabled once already reviewed) |
| **Fix it** | Replaces the local image from the top search result (`POST …/fix-image` or `…/fix-portrait`); sets **Fixed** and **Reviewed** |
| **More** | Opens a modal with up to **20** search results; click one to apply the same replace as **Fix it** |
| **Clear** | Deletes the local file(s), clears DB paths, leaves an **empty frame** (no placeholder); sets **Fixed** and **Reviewed** so on-demand fetch does not refill the slot |
| **Upload** | File picker for a local image; saves to disk like **Fix it** (thumbnail generated for paintings; portrait resized for artists) |
After **Fix it**, the detail image, gallery textures, and frame colour (gold if reviewed) update without a full page reload. **Back to Gallery** returns to the live hall session, not a stale snapshot.
After **Fix it**, **More**, **Upload**, or **Clear**, the main view, gallery textures (paintings), and timeline portrait (artists) update without a full page reload. Reviewed portraits show a gold border on the bio page; reviewed paintings use gold frames in the 3D hall. **Back to Gallery** returns to the live hall session, not a stale snapshot.
### Checkup page
@@ -264,7 +269,7 @@ After **Fix it**, the detail image, gallery textures, and frame colour (gold if
**Search visible** runs image search only for rows currently shown after text/filter — not automatically on page load. Fixing an image sets **Fixed** and **Reviewed**.
Run `npm run migrate:checkup-flags` once on existing databases. After server code changes, restart `npm run dev` so new routes (e.g. `PATCH …/checkup-flags`) are registered.
Run `npm run migrate:checkup-flags` and `npm run migrate:artist-checkup-flags` once on existing databases. After server code changes, restart `npm run dev` so new routes (e.g. clear, upload, portrait debug) are registered.
See [API.md](API.md#developer-image-audit) and [data-and-images.md](data-and-images.md#duplicate-paintings).
+15 -9
View File
@@ -336,19 +336,25 @@ As of a recent audit (~1200 paintings): **52 exact duplicate pairs** (52 removab
When **Debug mode** is on (home header) or from the **Checkup** page:
1. **Search**`GET /api/paintings/:id/debug-image-search` tries Google Custom Search (if `GOOGLE_CSE_API_KEY` + `GOOGLE_CSE_CX` are set in `.env`), Google Arts & Culture, Google Images scrape, then DuckDuckGo (`searchGoogleImagesFirst` in `scripts/image-fetcher.js`).
2. **Fix**`POST /api/paintings/:id/fix-image` downloads the chosen URL via `downloadImageForFix``replacePaintingImageFromUrl` in `server/image-service.js`, regenerates the thumbnail with `sharp`, and sets `checkup_fixed` + `checkup_checked`.
1. **Search**`GET /api/paintings/:id/debug-image-search` (or `…/debug-portrait-search` for artists) tries Google Custom Search (if `GOOGLE_CSE_API_KEY` + `GOOGLE_CSE_CX` are set in `.env`), Google Arts & Culture, Google Images scrape, then DuckDuckGo (`searchGoogleImagesFirst` / `searchArtistPortraitFirst` in `scripts/image-fetcher.js`).
2. **More**`GET …/debug-image-search/more` or `…/debug-portrait-search/more` returns up to 20 ranked candidates (`searchPaintingImagesMany` / `searchArtistPortraitMany`).
3. **Fix**`POST …/fix-image` or `…/fix-portrait` downloads the chosen URL via `downloadImageForFix``replacePaintingImageFromUrl` / `replaceArtistPortraitFromUrl` in `server/image-service.js`, regenerates thumbnails with `sharp`, and sets `checkup_fixed` + `checkup_checked`.
4. **Clear**`POST …/clear-image` or `…/clear-portrait` deletes local file(s), nulls DB paths, sets both flags. Cleared slots stay empty in the UI (no placeholder; `checkup_fixed` prevents on-demand refetch for paintings).
5. **Upload**`POST …/upload-image` or `…/upload-portrait` accepts a base64-encoded file (max 15 MB), validates with `sharp`, writes to the standard filename under `data/images/`.
### Painting detail debug panel
### Debug panel (painting detail and artist bio)
With debug mode on, `PaintingDetail.tsx` shows a bottom-left panel with search preview and two buttons:
With debug mode on, `PaintingDetail.tsx` and `ArtistBio.tsx` show a bottom-left panel with search preview and five buttons:
| Button | API | Effect |
|--------|-----|--------|
| **Checked** | `PATCH …/checkup-flags` `{ "checked": true }` | Marks reviewed; 3D frame turns gold |
| **Fix it** | `POST …/fix-image` | Saves image to disk, sets both flags, refreshes detail + gallery |
| Button | API (paintings / portraits) | Effect |
|--------|----------------------------|--------|
| **Checked** | `PATCH …/checkup-flags` `{ "checked": true }` | Marks reviewed; gold frame (paintings) or gold portrait border (artists) |
| **Fix it** | `POST …/fix-image` / `…/fix-portrait` | Saves top search result to disk, sets both flags, refreshes detail + gallery / timeline |
| **More** | `GET …/debug-*-search/more` then fix endpoint | Modal with 20 clickable results; pick one to replace |
| **Clear** | `POST …/clear-image` / `…/clear-portrait` | Removes file(s), empty frame in UI |
| **Upload** | `POST …/upload-image` / `…/upload-portrait` | Local file picker → save like **Fix it** |
The client passes `searchUrl`, `source`, and `thumbUrl` from the search result to improve download reliability. After a fix, `HomePage` updates the gallery session and appends a revision query on 3D texture URLs so replaced files reload even when the path is unchanged.
The client passes `searchUrl`, `source`, and `thumbUrl` from search results to improve download reliability. After a fix, clear, or upload, `HomePage` updates the gallery session and appends a revision query on texture URLs so replaced files reload even when the path is unchanged.
Checkup **Search visible** queues search for filtered rows only (3 concurrent); it does not search the full catalog on load.
+6 -2
View File
@@ -65,7 +65,8 @@ After a fresh seed, run these to match a fully populated local install:
npm run fetch-artist-bios # bio_short / bio_full from Wikipedia
npm run expand-catalog # famous works for artists below MIN_PAINTINGS
npm run update-influences # painting influence graph for detail view + hall exits
npm run migrate:checkup-flags # optional: review/fixed flags for Checkup page
npm run migrate:checkup-flags # optional: review/fixed flags for Checkup page (paintings)
npm run migrate:artist-checkup-flags # optional: same flags for artist portraits (bio debug)
npm run fetch-images -- --limit=50 # random sample; 10s max per painting (default)
npm run fetch-images -- --limit=50 --max-wait=120 # same batch size, longer lookup per work
cd client && npm run build && cd ..
@@ -114,6 +115,7 @@ Open http://localhost:3001 (or your configured `PORT`).
| `npm run sync-image-paths` | `scripts/sync-image-paths.js` | Align DB paths with disk *(if present)* |
| `npm run migrate:influence-sources` | `scripts/migrate-influence-sources.js` | Create `painting_influence_sources` + backfill legacy edges |
| `npm run migrate:checkup-flags` | `scripts/migrate-checkup-flags.js` | Add `checkup_checked` / `checkup_fixed` on `paintings` |
| `npm run migrate:artist-checkup-flags` | `scripts/migrate-artist-checkup-flags.js` | Add `checkup_checked` / `checkup_fixed` on `artists` (bio debug) |
| `npm run find-duplicates` | `scripts/find-duplicate-paintings.js` | Report duplicate and near-duplicate painting rows |
| `npm run update-influences` | `scripts/update-influences.js` | Insert influence links (painting / artist / movement) from `art-influences-data.js` |
| `npm run update-influences -- --fetch-images` | ↑ | Also download images for newly created works |
@@ -131,7 +133,8 @@ These are checked in and maintained:
- `influence-discovery.js` + `influence-resolver.js` — web discovery and polymorphic source resolution
- `fetch-missing-images.js` — batch image backfill
- `find-duplicate-paintings.js` — duplicate catalog audit
- `migrate-checkup-flags.js` — checkup workflow columns
- `migrate-checkup-flags.js` — checkup workflow columns (paintings)
- `migrate-artist-checkup-flags.js` — checkup workflow columns (artist portraits)
- `regenerate-thumbnails.js`, `audit-painting-images.js`
These are referenced in `package.json` but may need to be restored from git history if missing locally: `seed-wikipedia.js`, `fetch-artist-images.js`, `sync-image-paths.js`.
@@ -168,6 +171,7 @@ After clone: copy `.env.example` → `.env`, install dependencies, run `npm run
| Permission denied creating tables | `gallery` user lacks CREATE | Run admin grants, then migrate |
| Wikipedia API rate limit during fetch | Too many requests in a row | Wait and re-run; scripts retry with backoff |
| Checkup **Reviewed** toggle returns 404 | Stale server process missing new routes | Restart `npm run dev` after pulling API changes |
| Debug **More** / **Clear** / **Upload** returns 404 | Same as above | Restart server; routes live in `server/index.js` + `server/image-service.js` |
| **Fix it** fails with `read ECONNRESET` | Remote host dropped connection | Restart server; client sends `searchUrl` / `source`; retry or use Commons URL in overrides |
| Fixed image not shown in 3D gallery | Stale gallery session or cached texture | Rebuild client; fix updates session + `?v=` revision — use **Back to Gallery** (not browser back) |
| Frame still black after **Checked** | Gallery session not synced | Re-enter hall or toggle debug **Checked** from detail with gallery open behind overlay |