This commit is contained in:
Danila Khodjaef
2026-07-22 23:14:09 +03:00
482 changed files with 19800 additions and 1037 deletions
+188 -47
View File
@@ -8,10 +8,12 @@ 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 *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.
3. **3D gallery** — one personal hall per artist, a **movement gallery** (click a movement name), or a **guided tour** hall (timeline → **Tours**): period-themed or tour wings of up to ~55 works, side-wall hang, visit order left→right.
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**).
**Catalog search** — on the timeline home page, the header search bar (`CatalogSearchBar.tsx`) finds artists, paintings, and movements by name and metadata (year, movement, Wikipedia title). Type at least **2 characters** (300 ms debounce); results group into **Artists**, **Movements**, and **Paintings** with thumbnails. Keyboard: `↑`/`↓` to move, `Enter` to open, `Escape` to close. Choosing a result opens the artist gallery, movement gallery, or painting detail. Paintings opened from search show **← Back to Timeline** and return to the home timeline (full year range), not the previous view.
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).
## Stack
@@ -31,7 +33,8 @@ Gallery/
├── server/ # Express API, DB pool, image service
├── client/ # React/Vite frontend
│ ├── src/ # Source (components, pages, 3D scene)
│ │ ├── components/VirtualGallery.tsx # 3D hall (artist + movement modes)
│ │ ├── components/VirtualGallery.tsx # 3D hall (artist + movement modes; WebGL context-loss recovery)
│ │ ├── components/GalleryLoadingMarker.tsx # Loading spinner overlay/banner (catalog, portraits, halls)
│ │ ├── components/GalleryWindows.tsx # Side-wall daylight windows (movement)
│ │ ├── components/HallPassage.tsx # Open archway between movement wings
│ │ ├── components/MovementHallDetails.tsx # Period architectural details
@@ -44,8 +47,15 @@ Gallery/
│ │ ├── 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/CatalogSearchBar.tsx # Timeline header catalog search
│ │ ├── components/PaintingAnnotations.tsx # Art-history notes on painting detail
│ │ ├── pages/CheckupPage.tsx # Image audit table
│ │ ├── pages/TranslationsPage.tsx # Russian translation review
│ │ ├── pages/InfluencesPage.tsx # Influence links CRUD + import wizard
│ │ ├── pages/ToursPage.tsx # Guided tour editor
│ │ ├── components/ToursPopup.tsx # Public published-tours modal
│ │ ├── i18n/ # react-i18next bootstrap
│ │ └── locales/{en,ru}/ # UI chrome strings
│ │ ├── data/historical-events.ts # Timeline event markers (UI)
│ │ ├── data/movement-lineage.ts # Curated movement predecessor links (UI)
│ │ ├── utils/parquetFloorTexture.ts # Procedural parquet floor
@@ -53,6 +63,10 @@ Gallery/
│ │ └── utils/timelineView.ts # Shared zoom/pan math for timeline + movements
│ └── dist/ # Production build (served by API when present)
├── scripts/ # Seed, bios, catalog expansion, image fetch, checkup tools
│ ├── seed-wikipedia.js
│ ├── seed-catalog-data.js
│ ├── sync-image-paths.js
│ ├── fetch-artist-images.js
│ ├── fetch-artist-bios.js
│ ├── expand-paintings.js
│ ├── famous-paintings-data.js
@@ -66,42 +80,59 @@ Gallery/
├── Output/ # Generated exports (e.g. paintings.csv)
├── data/images/ # Local portraits and paintings (+ thumbs/)
├── db/ # schema.sql, setup-admin.sql, migrate-*.sql
├── server/migrate.js # npm run migrate — schema + incremental migrations
├── deploy/ # Production nginx + systemd examples
├── server/migrate.js # npm run dev:migrate — schema + incremental migrations
├── deploy/ # Legacy nginx + systemd examples (optional)
├── infra/docker/ # Production Dockerfile, TrueNAS compose, deploy scripts
├── Documentation/ # This folder
│ └── environments.md # Dev/prod URLs, DB split, sync, deploy
└── .env # DB and port config (not committed)
```
## Runtime modes
### Production-style (single process)
**Default:** develop and test on **dev** (`gallery_dev`, devgallery URL). **Production** is updated on a scheduled release (~weekly), not on every edit. See [environments.md — Development-first workflow](environments.md#development-first-workflow-default).
### Public development (`dev:web`) — primary
```bash
npm run start:prod # build client + serve on PORT (default 3520)
# or: npm run build && npm run start
npm run dev:web # Vite :5173 + API :3451 — https://devgallery.mysuperlab.netcraze.pro
```
Serves `/api/*`, `/images/*`, and the built SPA from `client/dist` if it exists.
Uses database **`gallery_dev`** on the same PostgreSQL host.
**Deployed URLs:** public http://gallery.mysuperlab.netcraze.pro · LAN http://192.168.10.70:3520 — see [setup.md](setup.md#production-deployment).
### Production (TrueNAS Docker) — scheduled releases
### Development (two processes)
Production runs in **`gallery-web`** on TrueNAS port **5173**, database **`gallery_prod`**, public URL **https://gallery.mysuperlab.netcraze.pro**. Images: `/mnt/BasePool/Applications/Gallery/data/images` (SMB share **`Gallery`**). Deploy via `npm run prod:docker:publish` and the promote checklist in [environments.md](environments.md).
### Production-style single process (local)
```bash
npm run dev:server # API on PORT from .env (3520 production, 3001 typical dev)
npm run dev:client # Vite on :5173, proxies /api and /images to PORT
npm run prod:start # build client + serve on PORT from .env
```
Use the Vite URL during frontend work for HMR.
Serves `/api/*`, `/images/*`, and the built SPA from `client/dist`.
### Local HMR (two processes)
```bash
npm run dev:server # API on PORT from .env
npm run dev:client # Vite on :5173, proxies /api and /images
```
Use for fast frontend iteration without Keenetic. Legacy nginx config in [`deploy/nginx-gallery.conf`](../deploy/nginx-gallery.conf) proxied the public domain to Vite `:5173`.
## User navigation flow
```mermaid
flowchart TD
A[Home — timeline + movement flow] -->|scroll / drag / zoom| A
A -->|catalog search| S[Search results dropdown]
S -->|artist| C
S -->|movement| G
S -->|painting| D
A -->|click movement name| G[Movement gallery — 3D wings]
G -->|click painting| D
D -->|Back| G
D -->|Back to Gallery| G
G -->|back door / Wings / Exit| H[Wing navigator]
H -->|pick wing| G
H -->|Exit to Timeline| A
@@ -113,16 +144,28 @@ flowchart TD
D -->|prev / next| D
D -->|click centre image| F[Fullscreen lightbox]
F -->|close| D
D -->|Back| C
D -->|Back to Gallery| C
C -->|exit doorway / E key| E[Path picker]
E -->|predecessors| C
E -->|successors| C
D -->|influence thumbnail| D
D -->|artist link| B
B -->|Back| A
C -->|Back| A
B -->|Back| C
C -->|Back to Timeline| A
D -->|Back to Timeline when opened from search| A
```
### Back navigation
| Control | Behaviour |
|---------|-----------|
| **← Back to Timeline** (3D gallery header, movement **Exit to Timeline**) | Always returns to the **home timeline**: unmounts the hall, clears the gallery session, resets timeline zoom to the full catalog year range |
| **← Back to Gallery** (painting detail from a hall) | Returns to the **same hall session** — camera position and wing are preserved |
| **← Back to Timeline** (painting detail opened from catalog search) | Returns to the home timeline (same as gallery **Back to Timeline**) |
| **← Back** (artist bio) | Returns to wherever you opened bio from (usually the artist hall) |
Implementation: `goToTimelineHome()` in `HomePage.tsx` — do not use the browser **Back** button; it is not wired to app navigation.
## Timeline and movement flow
The home page shows two linked views over the **same year window** (`viewStart` / `viewEnd` in `HomePage.tsx`):
@@ -132,12 +175,53 @@ 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`): 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.
Both views share zoom/pan behaviour via `client/src/utils/timelineView.ts` (`zoomTimelineView`, `panTimelineView`, `chooseTimelineTickInterval`, `createViewChangeScheduler`). 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.
### Catalog search (timeline header)
`CatalogSearchBar.tsx` calls `GET /api/search?q=…` (public, no login). The dropdown is stacked above the timeline (`z-index` on `.site-header`) so results are not hidden by movement bands.
| UX | Detail |
|----|--------|
| Minimum query | 2 characters after trim |
| Debounce | 300 ms |
| Result groups | Artists, Movements, Paintings (with thumb or movement colour swatch) |
| Open artist | Preload images → artist 3D hall |
| Open movement | Movement gallery (wing 1) |
| Open painting | Painting detail with `returnTo: timeline`**Back to Timeline** |
| Keyboard | `↑`/`↓` highlight, `Enter` open, `Escape` close |
Run `npm run dev:migrate:search` once on existing databases before first use, or rely on `npm run dev:migrate` (includes `migrate-search.sql`). See [API.md — GET /api/search](API.md#get-apsearch).
### Timeline data loading
On first visit, `HomePage.tsx` fetches the full catalog once:
1. `GET /api/bounds` — initialise the year range.
2. `GET /api/timeline?start=…&end=…` — all eras and movements for that range.
3. `GET /api/artists?timeline=1` — lightweight artist rows (portraits, lifespan, movement colour; no full biography text).
Pan, zoom, and era/event click-to-zoom only update **local** `viewStart` / `viewEnd` state. `MovementBands.tsx` and `Timeline.tsx` filter what is visible for the current window — they do not trigger new API calls.
**Loading indicators** (`GalleryLoadingMarker.tsx`) keep the user informed while data is still arriving:
| Marker | When |
|--------|------|
| Overlay **“Loading art history…”** | Until the first catalog fetch (`bounds` + `timeline` + `artists`) completes |
| Bottom banner **“Loading portraits…”** | While artist portrait thumbnails are still downloading on the movement flow (timeline stays interactive) |
| Overlay **“Opening artist/movement gallery…”** | Between clicking a portrait/movement and the 3D hall data being ready |
| Overlay **“Loading gallery…”** | While the 3D canvas initializes after the hall opens (center area; header and controls stay visible) |
| Overlay **“Loading paintings…”** | While wall textures are still downloading in the 3D hall |
| Overlay **“Restoring gallery…”** | Briefly after WebGL context loss while the canvas remounts |
View updates are **batched to one commit per animation frame** via `createViewChangeScheduler()` in `timelineView.ts` (`HomePage.tsx``handleViewChange`), so rapid scroll-wheel events do not flood React with separate renders.
### 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.
Label density is **dynamic**: `chooseTimelineTickInterval()` in `timelineView.ts` picks a “nice” step (1, 2, 5, 10, … 5000 years) from the visible span and measured bar width so labels stay ~76 px apart. Zoomed-out overviews show fewer dates; zooming in reveals finer steps automatically.
### Timeline controls
| Input | Action |
@@ -165,7 +249,7 @@ Each visible movement is drawn as a **portrait-width curved stream** (~54 px str
| Vertical depth | Successor movements sit on rows below their deepest parent; sibling movements at the same depth are spread into lanes to limit overlap |
| Branch connectors | Smooth curves from the **centre** of a parent stream to the **centre** of each child stream (siblings fan out along the parents length) |
| Visual blending | Path-aligned SVG gradients with transparent fades at stream ends and branch junctions; streams draw on top of branches so overlap brightness stays uniform |
| Filtering | Same rule as the API: only movements with at least one artist active in the visible year range |
| Filtering | A movement is drawn when its **span overlaps** the visible year range **and** it has at least one catalogued artist — artists whose lifespan falls outside the window still keep their movement visible (their portraits simply do not render). Filtered client-side after initial load |
| Viewport layout | Row height and stream width scale from measured canvas size so every visible movement row fits in the remaining screen space |
### Artists on movement streams
@@ -184,18 +268,22 @@ Each artist appears as a **portrait circle** on their movements stream row:
| Input | Action |
|-------|--------|
| Scroll wheel on flow canvas | Zoom (same range as timeline) |
| Scroll wheel anywhere on flow canvas | Zoom (same year range as timeline; works over portraits and labels too) |
| 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. Hovering a portrait highlights the artists lifespan on the era bar and brightens their segment on the movement stream.
**Zoom/pan performance:** The movement canvas uses a **capture-phase** wheel listener so scroll zoom works even when the cursor is over a portrait. While scrolling or dragging, a short **interaction mode** (~120 ms after the last input) draws simplified solid SVG strokes and temporarily hides portrait/label DOM so zoom stays responsive; full gradients and portraits return when you stop.
Only **mousedown** on portraits and movement labels stops propagation (so drag-to-pan does not start when clicking them). 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 live in **`painting_influence_sources`** (paintings, artists, or movements as sources). The API reads that table for detail panels, hall navigation, and `has_influence_links`. The legacy **`painting_influences`** table is still written in parallel when curators add painting-to-painting edges but is not queried for display.
## Virtual gallery (3D halls)
The 3D scene supports two modes in `VirtualGallery.tsx`: **artist halls** (personal catalog) and **movement galleries** (full movement collection, chronological).
The 3D scene supports three modes in `VirtualGallery.tsx`: **artist halls** (personal catalog), **movement galleries** (full movement collection, chronological), and **guided tours** (curator-ordered stops — [tours.md](tours.md)).
**Shared wall hang (all modes):** visit order fills the **left wall first**, then the **right**. The **first** work hangs near the entrance on the left (immediately left of the opening view); the **last** hangs near the entrance on the right. Artist halls use chronological order; movement wings use chronological order within each wing; tours use stop order.
### Artist halls
@@ -204,10 +292,9 @@ Each artist has **exactly one hall**. The hall is a rectangular room sized to fi
| Rule | Implementation |
|------|----------------|
| One hall per artist | `VirtualGallery.tsx` builds a single room from that artists paintings |
| Catalog depth | Most artists target **≥ 6** notable works via `npm run expand-catalog` and `famous-paintings-data.js`; some masters have larger museum dumps |
| Paintings on walls | Works hang on the **back, left, and right** walls in **one row per wall**; room **depth grows** when the catalog is large |
| Corridor layout | **15+ paintings:** short back wall (up to 8 works), remaining works on extended **left/right** side walls — a long gallery corridor |
| Wall order | On each wall, left → right: **later works on the left**, **earlier works on the right**; undated works sort toward the left |
| Catalog depth | Most artists target **≥ 6** notable works via `npm run dev:expand-catalog` and `famous-paintings-data.js`; some masters have larger museum dumps |
| Paintings on walls | Works hang on the **left and right** walls in **one row per wall**; room **depth grows** when the catalog is large (back wall is for the exit only) |
| Wall order | Chronological visit order: first half on the **left** (entrance → back), second half on the **right** (back → entrance); first work left of the opening view, last on the right |
| Floor | Herringbone **parquet** procedural texture (`parquetFloorTexture.ts`) |
| Wall tint | Gallery walls blend the artists **movement colour** into cream plaster tones |
| Frame finish | **Unchecked** works: black moulding; **Reviewed** (`checkup_checked`): bright gold moulding at **double width** |
@@ -232,7 +319,7 @@ Each artist has **exactly one hall**. The hall is a rectangular room sized to fi
| Click painting | Open detail view |
| Exit doorway / `E` / **Exit →** header button | Open path picker |
Predecessors and successors come from **`painting_influence_sources`** (painting and artist sources). Empty lists mean no influence edges are recorded yet for that artist — run `npm run update-influences` or extend `art-influences-data.js`.
Predecessors and successors come from **`painting_influence_sources`** (painting and artist sources). Empty lists mean no influence edges are recorded yet for that artist — run `npm run dev:update-influences` or extend `art-influences-data.js`.
### Movement galleries
@@ -243,7 +330,7 @@ Enter from the home page by clicking a **movement name** on the movement flow (`
| 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) |
| Wall order | Same shared hang as artist halls: first half left (entrance → back), second half right (back → entrance) |
| 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.) |
| Wall materials | Hi-res **procedural textures** with normal maps (`galleryProceduralTextures.ts`): real-world stone, marble, wood panelling, brick, velvet, stucco — plus **single-colour painted walls** (`painted-lime`, `painted-oil-matte`, `painted-oil-satin`, `painted-emulsion`, `painted-flat`) tinted per movement for Renaissance salons through modern white cubes |
@@ -254,7 +341,7 @@ Enter from the home page by clicking a **movement name** on the movement flow (`
| Front wall | Open **“Next wing →”** archway when a later wing exists; walk through or press `E` when near |
| Influence lamps | Same golden upside-down 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** |
| Detail return | **Back to Gallery** from painting detail returns to the same wing with camera preserved; **Back to Timeline** exits the hall entirely |
**Controls (movement gallery):**
@@ -267,17 +354,34 @@ Enter from the home page by clicking a **movement name** on the movement flow (`
Movement galleries do **not** use the predecessor/successor influence picker — that remains artist-hall only.
### Guided tour halls
Enter from the home page **Tours** popup (`GET /api/tours/:id`). Layout reuses the movement winged hall (`mode: 'tour'` in `VirtualGallery.tsx`).
| Rule | Implementation |
|------|----------------|
| Visit order | Curator `sort_order` on `tour_stops` (not chronological) |
| Wings | Same ~55-per-wing split as movements; order preserved across wings |
| Wall hang | Same left-then-right rule as other halls |
| Detail | Tour stop text panel; walks tour stops |
| Exit | Wing navigator / **Exit to Timeline** (no influence picker) |
Full guide: [tours.md](tours.md).
### 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.
**3D images** use locally cached files only (`galleryImageUrl` in `client/src/api/client.ts`). Remote fetches are too slow for realtime WebGL textures; the client calls `POST /api/artists/:id/preload-images` automatically when entering an **artist** hall (public route — links disk files only). 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. The center of the hall shows **“Loading gallery…”** until the WebGL canvas is ready, then **“Loading paintings…”** until every wall texture has resolved (tracked through `GalleryTextureLoadContext`). The 3D hall stays mounted while painting detail or bio overlays are open; returning remounts the canvas when the hall becomes active again.
**WebGL context-loss recovery:** on some GPUs/drivers (notably certain Chrome setups) the browser can drop the WebGL context right after entering a hall, which would otherwise leave a permanent dark window. `VirtualGallery.tsx` listens for `webglcontextlost` / `webglcontextrestored`, calls `preventDefault()` so the browser can restore the context, and remounts the `<Canvas>` with a fresh context (a **“Restoring gallery…”** overlay shows briefly). The network-loaded HDR `Environment` map is wrapped in an error boundary so, if it fails to load, the hall still renders without reflections instead of unmounting the whole scene.
## Painting detail view
Opened from the 3D hall (artist or movement wing — click a frame) or from influence thumbnails on another works detail page.
Opened from the 3D hall (artist, movement, or tour 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 (full work visible, letterboxed), artist portraits, or movement swatches — position in catalog (e.g. `3 of 12`) |
| **Tour notes** | When opened from a guided tour: stop text panel under the image (English body from `tour_stops`) |
| **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 |
@@ -285,47 +389,75 @@ Opened from the 3D hall (artist or movement wing — click a frame) or from infl
| Input | Action |
|-------|--------|
| `` / `` beside image | Previous / next work by the **same artist** (chronological order) |
| `` / `` beside image | Previous / next in the **current catalog** — artist chronology, movement chronology, or **tour stop order** |
| `←` / `→` | Same as prev / next (disabled while fullscreen is open) |
| 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** / **← Back to Timeline** | Return to the hall or movement wing you entered from — **3D camera position is preserved** |
| **← Back to Gallery** | Return to the hall or movement wing you entered from — **3D camera position is preserved** |
| **← Back to Timeline** | Return to the home timeline (from search result, or from the 3D gallery header / movement **Exit to Timeline**) — hall unmounts, timeline zoom resets |
| **About {artist}** | Open artist biography |
**Navigation rules:**
- **Catalog browsing** ( / arrow keys) walks the current artists works earliest → latest. It does **not** change the back target: after browsing several works, **Back to Gallery** still returns directly to the hall.
- **Catalog browsing** ( / arrow keys) walks the active catalog (artist / movement chronology, or tour stops). It does **not** change the back target: after browsing several works, **Back to Gallery** still returns directly to the hall.
- **Influence links** push a new detail layer; **Back** from an influenced work returns to the painting you came from (and from there back to the gallery if applicable).
- Side-panel influence images use **`object-fit: contain`** so tall or wide works are not cropped (dark letterbox background).
- The 3D hall stays mounted in the background while detail is open so nothing is lost on return.
### Debug mode (developer)
When **Debug mode** is enabled from the home header, painting detail and artist biography show a bottom-left panel with image search preview and action buttons. See [Developer tools (image audit)](#developer-tools-image-audit).
**Curator login required.** Debug tools are hidden until you sign in from the home header (**Curator login**). After login, enable **Debug mode** from the same header area.
When debug mode is on, painting detail and artist biography show a bottom-left panel with image search preview and action buttons. See [Developer tools (image audit)](#developer-tools-image-audit).
Next to the toggle, **Show more** (checkbox, persisted in `localStorage`) opens the **More** search-results modal automatically whenever you open a painting or artist bio while debug mode is on.
## Key design decisions
- **Timeline bounds** derive from the earliest art movement start year, not ancient-era metadata alone, so the default view opens where catalogued content begins.
- **Movement filtering** on zoom only shows movements that have at least one artist active in the visible year range.
- **Timeline catalog** loads once from the API; pan/zoom is client-side only, with per-frame batching via `createViewChangeScheduler()`.
- **Movement flow interaction** uses simplified SVG and hides portrait DOM during active scroll/drag so zoom stays responsive over dense portrait fields.
- **Movement filtering** on zoom shows movements whose span overlaps the visible year range and that have at least one catalogued artist; a movement (e.g. Byzantine or Gothic viewed at 3111231 CE) still appears even when all its artists lived outside the current window.
- **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.
- **Guided tours** add curator-ordered winged halls with stop text on painting detail — [tours.md](tours.md).
- **Wall hang** is shared: first work on the left at the entrance, last on the right.
- **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.
- **3D gallery images** use locally cached files only; slow remote fetches would break realtime rendering. The client calls `POST /api/artists/:id/preload-images` automatically when entering an **artist** hall (public route — links disk files only).
- **3D gallery session** stays mounted while painting detail or bio overlays are open; returning to the hall remounts the WebGL canvas when it becomes active again.
- **3D gallery resilience:** a lost WebGL context is recovered by remounting the canvas with a fresh context (rather than showing a dark window), and the HDR environment map is isolated behind an error boundary so its failure never blanks the scene.
- **Loading feedback:** `GalleryLoadingMarker` surfaces catalog load, portrait download, gallery entry, painting-texture load, and context-restore states so the user always knows work is still in progress.
- **Influence data** is stored in **`painting_influence_sources`** (directed links from paintings to source paintings, artists, or movements), with optional period fields and citation metadata. Sources include curated scholarship (`art-influences-data.js`) and **PainterPalette** (`discovered_via = painter-palette`). Legacy `painting_influences` mirrors painting-to-painting edges for scripts only.
## User roles and access
| Role | Who | Can do |
|------|-----|--------|
| **`user`** | Anonymous visitor (default) | Browse timeline, movement flow, 3D artist/movement halls, painting detail, artist bios, images |
| **`curator`** | Named account (`users` table) | Everything above + **Debug mode**, **Checkup**, **Translations**, **Influences**, **Tour editor**, debug API mutations |
Curators sign in via **Curator login** in the site header. Sessions use an HTTP-only cookie (`gallery.sid`). The UI hides debug controls from guests; the server enforces the same rules on debug/checkup API routes (`401` without a valid session).
Mutating debug actions (fix/clear/upload/delete, checkup flag changes) are appended to **`curator_audit_log`** with username, action, target id, optional JSON details, and client IP. Query in pgAdmin — see [DB_structure.md](DB_structure.md#curator_audit_log).
## Developer tools (image audit)
Optional workflow for curating local image files — not part of the public visitor experience.
Curator-only workflow for reviewing and fixing local image files — not part of the public visitor experience.
| Feature | Where | Purpose |
|---------|--------|---------|
| **Debug mode** | Home header toggle (`client/src/utils/debugMode.ts`) | Persists in `localStorage`; enables debug panel on painting detail and artist bio |
| **Show more** | Home header checkbox (same util) | When debug mode is on, auto-opens the **More** modal on each painting / bio page load |
| **Checkup page** | Home header **Checkup** (`CheckupPage.tsx`) | Full-catalog table: gallery vs detail thumbnails, search, fix, review flags |
| **Catalog search** | Timeline header (all visitors) | Find artists, paintings, movements; `GET /api/search`; navigate to gallery or detail |
| **Curator login** | Home header (guests) | Username + password modal; unlocks debug tools |
| **Debug mode** | Home header toggle (curators only) | Persists in `localStorage`; enables debug panel on painting detail and artist bio |
| **Show more** | Home header checkbox (curators, when debug on) | Auto-opens the **More** modal on each painting / bio page load |
| **Checkup page** | Home header → **Checkup** (curators only) | Full-catalog table: gallery vs detail thumbnails, search, fix, review flags |
| **Translations** | Home header → **Translations** (curators only) | Review/publish Russian `entity_translations` |
| **Influences** | Home header → **Influences** (curators only) | List/CRUD influence edges, CSV/JSON/XLSX import wizard, neighborhood graph — [influence-import.md](influence-import.md) |
| **Tour editor** | Home header → **Tour editor** (curators only) | Create/publish guided tours and stop text — [tours.md](tours.md) |
| **Tours** | Home header → **Tours** (everyone) | Open published tours in a 3D hall — [tours.md](tours.md) |
| **Logout** | Home header (curators) | Ends session; hides debug tools |
| **Debug panel** | Painting detail or artist bio (bottom-left, when debug mode on) | Search preview + action buttons (six on painting detail, five on artist bio) |
### Debug panel (painting detail and artist bio)
@@ -335,13 +467,15 @@ When debug mode is on, a panel at the bottom-left shows the image search query,
| Button | Painting detail | Artist bio |
|--------|-----------------|------------|
| **Checked** | Sets `checkup_checked` via `PATCH …/checkup-flags` | Same for artist portrait flags |
| **Fix it** | Replaces local image from top search result | Replaces portrait |
| **More** | Modal with up to **20** results (resolution shown when known) | Same |
| **Fix it** | Replaces full image from search result; **regenerates painting thumb** (~400px JPEG) from that file | Replaces portrait; **regenerates timeline thumb** (256px) |
| **More** | Modal with up to **20** results (resolution shown when known); thumb regenerated from chosen full image | Same |
| **Clear** | Deletes files, clears DB paths, empty frame | Clears portrait slot |
| **Upload** | Local file picker → disk + thumbnail | Local file → portrait |
| **Upload** | Local file picker (`DebugUploadButton`) → full image + **auto-generated painting thumb**; full-page **Loading…** overlay; hides current image and pauses search/fix until upload finishes | Local file → portrait + **auto-generated portrait thumb**; same upload overlay behaviour |
| **Remove entry** | **Painting detail only** — deletes row from DB, removes image files, refreshes 3D gallery, navigates to next/previous work in catalog (or back to gallery if last work). No confirmation dialog. | — |
After **Fix it**, **More**, **Upload**, or **Clear**, the main view, gallery textures (paintings), and timeline portrait (artists) update without a full page reload. **Remove entry** refetches artist (and movement gallery when relevant) from the API and remounts the 3D hall so the deleted frame disappears immediately.
After **Fix it**, **More**, **Upload**, or **Clear**, the main view, gallery textures (paintings), and timeline portrait (artists) update without a full page reload. Painting and portrait thumbs under `data/images/*/thumbs/` are rebuilt on the server whenever a curator replaces the full image. **Remove entry** refetches artist (and movement gallery when relevant) from the API and remounts the 3D hall so the deleted frame disappears immediately.
Pressing **Upload** clears the debug search preview and closes **More** before the file picker opens. While uploading, **Fix it**, **More**, and **Checked** are disabled and the main painting/portrait is hidden behind a centered loading overlay.
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.
@@ -353,15 +487,22 @@ Influence side-panel thumbnails use **letterboxing** (`object-fit: contain`) so
**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`, `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 start` (or `npm run dev:server`) so new routes (e.g. clear, upload, delete painting, 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.
Run `npm run dev:migrate:checkup-flags`, `npm run dev:migrate:artist-checkup-flags`, and `npm run dev:migrate:painting-annotations` once on existing databases. Load notes with `npm run dev:update-painting-annotations` (add `--wikipedia` for overview lines from Wikipedia intro text). After server code changes, restart `npm run dev:start` (or `npm run dev:server`) so new routes 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).
Server-side auth lives in `server/middleware/session.js`, `server/middleware/auth.js`, `server/routes/auth.js`, and `server/audit-log.js`. Client auth context: `client/src/context/AuthContext.tsx`.
See [API.md](API.md#authentication) and [data-and-images.md](data-and-images.md#duplicate-paintings).
## Related docs
| Document | Contents |
|----------|----------|
| [setup.md](setup.md) | Install, database, npm scripts |
| [deploy-dev-to-prod.md](deploy-dev-to-prod.md) | Release runbook + one-command `devtoprod:release` |
| [harmonize-dev-prod.md](harmonize-dev-prod.md) | Incremental dev ↔ prod merge (catalog DB + images) |
| [DB_structure.md](DB_structure.md) | Tables and relationships |
| [API.md](API.md) | REST endpoints |
| [influence-import.md](influence-import.md) | Curator Influences tool — import wizard, CRUD, graph |
| [tours.md](tours.md) | Guided tours — editor, public popup, 3D tour hall |
| [i18n-russian.md](i18n-russian.md) | Russian UI + entity_translations |
| [data-and-images.md](data-and-images.md) | Image pipeline and seeding |