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>
This commit is contained in:
Danila Khodjaef
2026-06-21 16:54:19 +03:00
co-authored by Cursor
parent 0972b5df99
commit df29848d89
121 changed files with 4765 additions and 396 deletions
+72 -5
View File
@@ -76,6 +76,54 @@ Artists belonging to a single movement.
---
## `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**
```json
{
"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.
@@ -152,11 +200,13 @@ Up to 20 ranked portrait candidates for the **More** picker modal.
"searchUrl": "https://…",
"source": "google-custom-search",
"results": [
{ "imageUrl": "https://…", "thumbUrl": "https://…", "source": "google-custom-search" }
{ "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`
@@ -206,7 +256,7 @@ Upload a local image (base64 JSON body). Validates with `sharp`, resizes to port
}
```
Max size 15 MB. **Response** — same as `fix-portrait`.
Max decoded size 15 MB (JSON body limit 20 MB on the server). **Response** — same as `fix-portrait`.
---
@@ -286,7 +336,22 @@ Painting detail with influence graph neighbours.
"period_start_year": 1905, "period_end_year": 1907
}
],
"influenced": [ { "source_type": "painting", "id": 20, "title": "...", ... } ]
"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"
}
]
}
```
@@ -427,11 +492,13 @@ Up to 20 ranked painting image candidates for the **More** picker modal.
"searchUrl": "https://…",
"source": "google-arts",
"results": [
{ "imageUrl": "https://…", "thumbUrl": "https://…", "source": "google-arts" }
{ "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`
@@ -464,7 +531,7 @@ Upload a local painting image (base64 JSON body). Validates with `sharp`, writes
}
```
Max size 15 MB. **Response** — same shape as `fix-image` (`imagePath`, `thumbnailPath`, `fixed`, `checked`).
Max decoded size 15 MB (JSON body limit 20 MB on the server). **Response** — same shape as `fix-image` (`imagePath`, `thumbnailPath`, `fixed`, `checked`).
---
+20
View File
@@ -109,6 +109,26 @@ When `checkup_fixed` is true, `checkup_checked` is set automatically and cannot
Applied by `npm run migrate:checkup-flags` (`db/migrate-checkup-flags.sql`).
### `painting_annotations`
Short art-history notes shown on painting detail (`PaintingAnnotations.tsx`).
| Column | Type | Notes |
|--------|------|-------|
| `id` | SERIAL PK | |
| `painting_id` | FK → `paintings` | ON DELETE CASCADE |
| `label` | VARCHAR(80) | Optional short heading (e.g. figure name) |
| `body` | TEXT | Note text |
| `category` | VARCHAR(30) | Default `subject`; also `technique`, `context`, `symbolism`, etc. |
| `pos_x`, `pos_y` | NUMERIC(5,2) | Optional marker position on image (percent 0100) |
| `source_author` | VARCHAR(200) | e.g. Gombrich, Met catalog |
| `source` | VARCHAR(500) | Citation label |
| `source_url` | VARCHAR(500) | Reference link |
| `sort_order` | INTEGER | Display order within the painting |
| `confidence` | VARCHAR(20) | Default `curated`; Wikipedia pass uses `wikipedia` |
Applied by `npm run migrate:painting-annotations` (`db/migrate-painting-annotations.sql`). Load data with `npm run update-painting-annotations` (curated entries in `scripts/painting-annotations-data.js`; add `--wikipedia` for intro sentences from each works `wikipedia_title`).
### `painting_influences`
Directed edges: *this painting* was influenced by *that painting*.
+76 -12
View File
@@ -1,6 +1,6 @@
# Art Gallery — architecture basics
Interactive virtual museum spanning art history: zoomable timeline, branching movement flow, 3D gallery halls, and painting influence graphs. See [README.md](../README.md) for quick start.
Interactive virtual museum spanning art history: zoomable timeline with event guides, branching movement flow, 3D gallery halls, painting influence graphs, and art-history annotations on detail pages. See [README.md](../README.md) for quick start.
## Concept
@@ -8,8 +8,8 @@ The app is organised as a **drill-down hierarchy**:
1. **Timeline** — historical eras (Ancient → Contemporary) with definite or fuzzy date boundaries.
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.
3. **3D gallery** — one personal hall per artist *or* a **movement gallery** (click a movement name on the flow diagram): period-themed interiors, chronological wings of up to ~55 works, side-wall hang only.
4. **Painting detail** — full work in the centre, *Influenced By* on the left, *Influenced* on the right (paintings, artists, or movements), optional **art-history notes** with image markers, 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`). 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).
@@ -31,11 +31,20 @@ Gallery/
├── server/ # Express API, DB pool, image service
├── client/ # React/Vite frontend
│ ├── src/ # Source (components, pages, 3D scene)
│ │ ├── components/VirtualGallery.tsx # 3D hall (parquet, frames, museum exit)
│ │ ├── components/VirtualGallery.tsx # 3D hall (artist + movement modes)
│ │ ├── components/GalleryWindows.tsx # Side-wall daylight windows (movement)
│ │ ├── components/HallPassage.tsx # Open archway between movement wings
│ │ ├── components/MovementHallDetails.tsx # Period architectural details
│ │ ├── data/movement-interior-styles.ts # Per-movement interior themes
│ │ ├── utils/movementHallLayout.ts # Wing split + window gap placement
│ │ ├── utils/galleryProceduralTextures.ts # Hi-res wall/floor textures
│ │ ├── components/PaintingDetail.tsx # Detail view + debug panel
│ │ ├── components/ArtistBio.tsx # Biography + portrait debug panel
│ │ ├── components/DebugSearchResultsModal.tsx # “More” search picker (20 results)
│ │ ├── components/Timeline.tsx # Era bar, year ticks, event markers
│ │ ├── components/TimelineEventGuides.tsx # Event vertical guides into movement flow
│ │ ├── components/MovementBands.tsx # Movement flow (SVG streams + branches)
│ │ ├── components/PaintingAnnotations.tsx # Art-history notes on painting detail
│ │ ├── pages/CheckupPage.tsx # Image audit table
│ │ ├── data/historical-events.ts # Timeline event markers (UI)
│ │ ├── data/movement-lineage.ts # Curated movement predecessor links (UI)
@@ -80,6 +89,14 @@ Use the Vite URL during frontend work for HMR.
```mermaid
flowchart TD
A[Home — timeline + movement flow] -->|scroll / drag / zoom| A
A -->|click movement name| G[Movement gallery — 3D wings]
G -->|click painting| D
D -->|Back| G
G -->|back door / Wings / Exit| H[Wing navigator]
H -->|pick wing| G
H -->|Exit to Timeline| A
G -->|front arch / E| G
G -->|Back to Timeline| A
A -->|click portrait| B[Artist bio — Wikipedia text]
B -->|Enter Gallery| C[Artist hall — 3D]
C -->|click painting| D[Painting detail + influences]
@@ -105,7 +122,11 @@ The home page shows two linked views over the **same year window** (`viewStart`
| Era bar | `Timeline.tsx` | Historical eras, major event markers, click-to-zoom |
| Movement flow | `MovementBands.tsx` | Curved streams per movement, lineage branches, artist portraits |
Both views share zoom/pan behaviour via `client/src/utils/timelineView.ts`. The home page uses a **fixed viewport** (`100vh`): the movement flow compresses vertically so all movements in the visible year range fit without page scrolling.
Both views share zoom/pan behaviour via `client/src/utils/timelineView.ts`. The home page uses a **fixed viewport** (`100vh`): timeline + movement flow sit in a shared `home-timeline-stack` so event guide lines can extend from the era bar down through the movement canvas. The movement flow compresses vertically so all movements in the visible year range fit without page scrolling.
### Timeline year labels
Year ticks along the bottom of the era bar use **large, high-contrast** labels (bold cream text with shadow). The active range in the control row (e.g. `1400 CE — 1900 CE`) uses the same stronger styling.
### Timeline controls
@@ -122,6 +143,8 @@ Both views share zoom/pan behaviour via `client/src/utils/timelineView.ts`. The
Major world events appear on the era bar as pin markers (single years) or shaded spans (e.g. World War I, World War II). Data lives in `client/src/data/historical-events.ts` — not in PostgreSQL. Labels appear when zoomed in enough; tooltips always show name and dates. Edit `HISTORICAL_EVENTS` and rebuild the client to add or change markers.
**Vertical guides:** `TimelineEventGuides.tsx` draws faint gold lines (point events) or shaded bands (spans) from the marker row **down through the movement flow**, aligned to the same year scale. Guides are visual only (`pointer-events: none`); click-to-zoom stays on the markers in `Timeline.tsx`.
### Movement flow
Each visible movement is drawn as a **portrait-width curved stream** (~54 px stroke, compressed when many rows are visible) from its start year to its end year.
@@ -154,13 +177,18 @@ Each artist appears as a **portrait circle** on their movements stream row:
| Scroll wheel on flow canvas | Zoom (same range as timeline) |
| Drag on flow canvas | Pan |
| Click portrait | Open artist biography |
| Click **movement name** (label on stream) | Open **movement gallery** for that movement |
Artist portraits stop wheel/drag propagation so zooming over a face does not fight portrait clicks.
Artist portraits stop wheel/drag propagation so zooming over a face does not fight portrait clicks. Hovering a portrait highlights the artists lifespan on the era bar and brightens their segment on the movement stream.
**Note:** Movement lineage is **frontend curation** for layout and labels — it is not stored in PostgreSQL. Painting influence links (`painting_influence_sources`, plus legacy `painting_influences` for hall navigation) are separate and drive the 3D exit picker and detail panels.
## Virtual gallery (3D halls)
The 3D scene supports two modes in `VirtualGallery.tsx`: **artist halls** (personal catalog) and **movement galleries** (full movement collection, chronological).
### Artist halls
Each artist has **exactly one hall**. The hall is a rectangular room sized to fit their catalog:
| Rule | Implementation |
@@ -182,7 +210,7 @@ Each artist has **exactly one hall**. The hall is a rectangular room sized to fi
| Detail view return | Opening a painting close-up **keeps the 3D hall mounted** in the background so position and view direction are preserved when you go back |
| After image fix | Debug **Fix it** updates the gallery session, busts texture cache (`?v=N`), and returns to the hall with the new image and gold frame |
**Controls:**
**Controls (artist hall):**
| Input | Action |
|-------|--------|
@@ -196,15 +224,50 @@ Each artist has **exactly one hall**. The hall is a rectangular room sized to fi
Predecessors and successors come from the **painting influence graph** (`painting_influences` → other artists). Empty lists mean no influence edges are recorded yet for that artist — run `npm run update-influences` or extend seed data.
**3D images** use locally cached files only (`galleryImageUrl` in `client/src/api/client.ts`). Remote fetches are too slow for realtime WebGL textures; call `POST /api/artists/:id/preload-images` before entering a hall to link disk files. While a texture is loading, the frame shows the canvas cover instead of a white placeholder.
### Movement galleries
Enter from the home page by clicking a **movement name** on the movement flow (`MovementBands.tsx``GET /api/movements/:id/gallery`).
| Rule | Implementation |
|------|----------------|
| One gallery per movement | All paintings by artists in that movement, sorted chronologically |
| Wings | Catalog split into wings of up to **55 works** (`movementHallLayout.ts`); large movements (e.g. Baroque) use multiple wings |
| Paintings on walls | **Left and right walls only** — back wall reserved for exit, front for passage to the next wing |
| Wall order | Along each side wall: **later works on the left**, **earlier on the right** (same convention as artist halls) |
| Frame captions | **Year · artist** label below each frame |
| Period interior | Each of the 26 seeded movements maps to a unique style in `movement-interior-styles.ts` (Italian palazzo, Baroque palace, NYC loft, white cube, etc.) |
| Textures | Hi-res procedural wall/floor/ceiling maps with normal maps (`galleryProceduralTextures.ts`) |
| Windows | **Side walls only** — placed in gaps between frames (high on the wall, no overlap with paintings); style matches the movement era |
| Lighting | Daylight from windows + ceiling track lights + ambient/sun fill |
| Back wall | **Exit double doors****Wing navigator** (jump to any wing) or **Exit to Timeline** |
| Front wall | Open **“Next wing →”** archway when a later wing exists; walk through or press `E` when near |
| Influence lamps | Same golden lamps as artist halls when `has_influence_links` is true |
| Missing images | Draped canvas cover in frame |
| Detail return | Hall stays mounted; camera preserved on **Back to Timeline** / **Back to Gallery** |
**Controls (movement gallery):**
| Input | Action |
|-------|--------|
| Walk / turn / drag | Same as artist hall |
| Click painting | Open detail view (returns to the same wing) |
| Back wall / `E` / **Wings / Exit →** | Open wing navigator |
| Front archway / `E` (when near) | Advance to the **next chronological wing** |
Movement galleries do **not** use the predecessor/successor influence picker — that remains artist-hall only.
### Shared 3D behaviour
**3D images** use locally cached files only (`galleryImageUrl` in `client/src/api/client.ts`). Remote fetches are too slow for realtime WebGL textures; call `POST /api/artists/:id/preload-images` before entering an **artist** hall to link disk files. Movement galleries load painting lists from the API without a separate preload step. While a texture is loading, the frame shows the canvas cover instead of a white placeholder.
## Painting detail view
Opened from the 3D hall (click a frame) or from influence thumbnails on another works detail page.
Opened from the 3D hall (artist or movement wing — click a frame) or from influence thumbnails on another works detail page.
| Layer | What you see |
|-------|----------------|
| **Detail** | Centre image, *Influenced By* (left) and *Influenced* (right) — painting thumbnails, artist portraits, or movement swatches — position in catalog (e.g. `3 of 12`) |
| **Art history notes** | Numbered markers on the image (when positioned) plus a note list below — short citations from Gombrich, museum catalogs, Wikipedia, etc. (`painting_annotations` table) |
| **Fullscreen** | Click the centre image; `Escape` or click anywhere to return to detail only |
**Controls:**
@@ -216,7 +279,7 @@ Opened from the 3D hall (click a frame) or from influence thumbnails on another
| Click centre image | Open fullscreen lightbox |
| Click influence thumbnail | Open that works detail (different artist allowed) |
| Click influence artist portrait | Open that artists 3D gallery hall |
| **← Back to Gallery** | Return to the hall you entered from — **3D camera position is preserved** |
| **← Back to Gallery** / **← Back to Timeline** | Return to the hall or movement wing you entered from — **3D camera position is preserved** |
| **About {artist}** | Open artist biography |
**Navigation rules:**
@@ -235,6 +298,7 @@ When **Debug mode** is enabled from the home header, painting detail and artist
- **Movement filtering** on zoom only shows movements that have at least one artist active in the visible year range.
- **Movement lineage** (`movement-lineage.ts`) documents art-historical predecessor→successor links for the flow diagram; extend that file to add or correct branches.
- **One hall per artist** keeps navigation predictable: enter from the timeline or bio, leave via the single exit or back button.
- **Movement galleries** complement artist halls: full movement corpus in period-themed wings, entered from the flow diagram.
- **Influence-based hall links** connect artists through documented painting relationships, grouped by movement at the exit.
- **3D gallery images** use locally cached files only; slow remote fetches would break realtime rendering.
- **Influence data** is stored as directed links from paintings to sources (another painting, an artist, or a movement), with optional period fields and citation metadata (source author, quote, URL).
@@ -257,7 +321,7 @@ When debug mode is on, a panel at the bottom-left shows the image search query,
|--------|--------|
| **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** |
| **More** | Opens a modal with up to **20** search results (each shows image resolution when available); 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) |
@@ -269,7 +333,7 @@ After **Fix it**, **More**, **Upload**, or **Clear**, the main view, gallery tex
**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` 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.
Run `npm run migrate:checkup-flags`, `npm run migrate:artist-checkup-flags`, and `npm run migrate:painting-annotations` once on existing databases. Load notes with `npm run update-painting-annotations` (add `--wikipedia` for overview lines from Wikipedia intro text). After server code changes, restart `npm run dev` so new routes (e.g. clear, upload, portrait debug, annotations) are registered. JSON body limit for uploads is **20 MB** (`express.json` in `server/index.js`); individual files are capped at **15 MB** after decode.
See [API.md](API.md#developer-image-audit) and [data-and-images.md](data-and-images.md#duplicate-paintings).
+28 -4
View File
@@ -191,6 +191,12 @@ Separate from the painting influence graph, `client/src/data/movement-lineage.ts
To add or fix a branch, edit `MOVEMENT_LINEAGE` in that file and rebuild the client. No migration or API change is required.
## Movement gallery interiors (frontend)
Separate from movement lineage layout, `client/src/data/movement-interior-styles.ts` defines a **unique 3D interior** for each seeded art movement (26 styles): wall/floor/ceiling textures, trim colours, window style, and architectural details (columns, coffered ceilings, etc.). Textures are generated procedurally in `client/src/utils/galleryProceduralTextures.ts`.
Wing layout (up to 55 works per wing, side-wall-only hang, window gap placement) lives in `client/src/utils/movementHallLayout.ts`. To change a movements look, edit its entry in `movement-interior-styles.ts` and rebuild the client.
## Historical event markers (frontend timeline)
`client/src/data/historical-events.ts` lists **world-history** markers shown on `Timeline.tsx` (French Revolution, World War I/II, etc.).
@@ -200,9 +206,25 @@ To add or fix a branch, edit `MOVEMENT_LINEAGE` in that file and rebuild the cli
| Storage | TypeScript module in the client — **not** a database table |
| Format | `{ id, name, startYear, endYear?, shortLabel? }` — omit `endYear` for a single-year pin |
| Interaction | Click a marker to zoom the shared timeline/movement view to that period |
| Vertical guides | `TimelineEventGuides.tsx` draws faint gold lines (or shaded spans) from the marker row down through the movement flow, aligned to the same year scale |
Edit `HISTORICAL_EVENTS` and rebuild the client to extend the set.
## Painting annotations (art-history notes)
Short curator-style notes on the painting detail page — separate from the influence graph.
| Aspect | Detail |
|--------|--------|
| Storage | PostgreSQL table `painting_annotations` |
| UI | `PaintingAnnotations.tsx` — numbered markers on the image (when `pos_x` / `pos_y` set) plus an “Art history notes” list |
| API | Included as `annotations[]` on `GET /api/paintings/:id` |
| Curated data | `scripts/painting-annotations-data.js` — artist/title keys matched via `influence-resolver.js` |
| Load | `npm run update-painting-annotations` (replaces existing rows per painting by default) |
| Wikipedia pass | `npm run update-painting-annotations -- --wikipedia` — one intro sentence per work from `wikipedia_title`; use `--wiki-delay=3000` if rate-limited; `--no-replace` to append without clearing curated rows |
Categories include `subject`, `technique`, `context`, and `symbolism`. Sources cite Gombrich, museum catalogs, and Wikipedia as appropriate.
## Batch image fetch
`npm run fetch-images` (alias: `npm run search-missing-paintings`) runs `scripts/fetch-missing-images.js`. It searches multiple sources for paintings without local files:
@@ -265,7 +287,9 @@ Requests are deduplicated (`inflight` map) and timeout after 15 seconds. On-dema
## Preload before 3D gallery
`POST /api/artists/:id/preload-images` runs **local-only** linking — no network. Call this when entering an artists 3D hall so textures use files already on disk.
`POST /api/artists/:id/preload-images` runs **local-only** linking — no network. Call this when entering an **artists** 3D hall so textures use files already on disk.
**Movement galleries** (`GET /api/movements/:id/gallery`) do not use preload — they load the full painting list from the API and resolve local paths the same way as artist halls. Works without files still show the canvas cover in the frame.
The 3D scene uses `galleryImageUrl()`, which never hits the on-demand API (remote latency breaks WebGL texture loading).
@@ -337,10 +361,10 @@ 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` (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`).
2. **More**`GET …/debug-image-search/more` or `…/debug-portrait-search/more` returns up to 20 ranked candidates (`searchPaintingImagesMany` / `searchArtistPortraitMany`). The modal shows each thumbnail with **resolution** when the search API provides dimensions; otherwise the client probes via `GET /api/debug/image-proxy`.
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/`.
5. **Upload**`POST …/upload-image` or `…/upload-portrait` accepts a base64-encoded file in JSON (Express body limit **20 MB**; decoded image max **15 MB**), validates with `sharp`, writes to the standard filename under `data/images/`.
### Debug panel (painting detail and artist bio)
@@ -350,7 +374,7 @@ With debug mode on, `PaintingDetail.tsx` and `ArtistBio.tsx` show a bottom-left
|--------|----------------------------|--------|
| **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 |
| **More** | `GET …/debug-*-search/more` then fix endpoint | Modal with 20 clickable results (resolution label under each thumb); 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** |
+12
View File
@@ -67,6 +67,8 @@ npm run expand-catalog # famous works for artists below MIN_PAINTING
npm run update-influences # painting influence graph for detail view + hall exits
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 migrate:painting-annotations # optional: art-history notes table
npm run update-painting-annotations # optional: load curated notes (+ --wikipedia for Wikipedia intros)
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 ..
@@ -116,6 +118,10 @@ Open http://localhost:3001 (or your configured `PORT`).
| `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 migrate:painting-annotations` | `scripts/migrate-painting-annotations.js` | Create `painting_annotations` table |
| `npm run update-painting-annotations` | `scripts/update-painting-annotations.js` | Load curated notes from `painting-annotations-data.js` |
| `npm run update-painting-annotations -- --wikipedia` | ↑ | Add intro sentences from each works Wikipedia page |
| `npm run update-painting-annotations -- --wikipedia --wiki-delay=3000` | ↑ | Slower Wikipedia pass when rate-limited (429) |
| `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 |
@@ -135,6 +141,7 @@ These are checked in and maintained:
- `find-duplicate-paintings.js` — duplicate catalog audit
- `migrate-checkup-flags.js` — checkup workflow columns (paintings)
- `migrate-artist-checkup-flags.js` — checkup workflow columns (artist portraits)
- `migrate-painting-annotations.js`, `update-painting-annotations.js`, `painting-annotations-data.js` — art-history notes on painting detail
- `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`.
@@ -172,7 +179,12 @@ After clone: copy `.env.example` → `.env`, install dependencies, run `npm run
| 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` |
| Debug **Upload** returns 413 Payload Too Large | Base64 JSON exceeds body limit | Server allows 20 MB JSON / 15 MB decoded image; compress file or resize before upload |
| No art-history notes on painting detail | Annotations not migrated or loaded | `npm run migrate:painting-annotations` then `npm run update-painting-annotations` |
| **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 |
| Duplicate works in gallery / timeline | Double import or variant Wikipedia titles | `npm run find-duplicates`; merge or delete spare rows manually |
| **Failed to load movement gallery** / `Cannot GET /api/movements/:id/gallery` | Stale server process missing route | Restart `npm run dev` or `npm run dev:server` after pulling API changes |
| Movement gallery shows generic cream walls | Stale client build | `cd client && npm run build`; hard-refresh browser |
| Windows overlap paintings in movement wing | Stale client | Rebuild client — windows are placed only on side walls in gaps between frames |