Document gallery loading markers, WebGL recovery, and movement filtering.

Update basics.md for the new loading indicators, 3D WebGL context-loss recovery, and the corrected movement-visibility rule (span overlap plus any catalogued artist). Refresh remaining docs and config for the current dev-first workflow and npm script names.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-07-07 12:36:34 +03:00
co-authored by Cursor
parent 1408811948
commit a2263ca186
10 changed files with 288 additions and 275 deletions
+4 -4
View File
@@ -15,16 +15,16 @@ alwaysApply: true
| Database | `gallery_dev` via root `.env` (`DB_NAME=gallery_dev`) | | Database | `gallery_dev` via root `.env` (`DB_NAME=gallery_dev`) |
| Data / images | `data/images/` in the repo | | Data / images | `data/images/` in the repo |
| Public URL | https://devgallery.mysuperlab.netcraze.pro | | Public URL | https://devgallery.mysuperlab.netcraze.pro |
| Migrations / seeds / scripts | `npm run migrate`, `npm run seed`, etc. against **dev** | | Migrations / seeds / scripts | `npm run dev:migrate`, `npm run dev:seed`, etc. against **dev** |
## Production is scheduled, not daily ## Production is scheduled, not daily
Prod (`gallery_prod`, TrueNAS Docker, https://gallery.mysuperlab.netcraze.pro) is updated **roughly weekly** (or when the user explicitly asks for prod): Prod (`gallery_prod`, TrueNAS Docker, https://gallery.mysuperlab.netcraze.pro) is updated **roughly weekly** (or when the user explicitly asks for prod):
1. Validate on dev 1. Validate on dev
2. `npm run db:backup` → `npm run db:restore:prod` (if DB/data changed) 2. `npm run dev:db:backup` → `npm run devtoprod:db:restore` (if DB/data changed)
3. `npm run images:sync-to-prod` (if images changed) 3. `npm run devtoprod:images` (if images changed)
4. `npm run docker:publish` (if code changed) 4. `npm run prod:docker:publish` (if code changed)
5. Restart **gallery-web** on TrueNAS 5. Restart **gallery-web** on TrueNAS
Do **not** edit `infra/docker/.env.prod`, run `db:restore:prod`, `images:sync-to-prod`, or `docker:publish` unless the user clearly targets prod. Do **not** edit `infra/docker/.env.prod`, run `db:restore:prod`, `images:sync-to-prod`, or `docker:publish` unless the user clearly targets prod.
+1 -1
View File
@@ -15,7 +15,7 @@ TRUST_PROXY=true
IMAGE_DIR=./data/images IMAGE_DIR=./data/images
# Curator auth (run npm run migrate after setting CURATOR_PASSWORD) # Curator auth (run npm run dev:migrate after setting CURATOR_PASSWORD)
SESSION_SECRET=change-me-to-a-long-random-string SESSION_SECRET=change-me-to-a-long-random-string
SESSION_COOKIE_SECURE=false SESSION_COOKIE_SECURE=false
CURATOR_USERNAME=curator CURATOR_USERNAME=curator
+2 -2
View File
@@ -279,9 +279,9 @@ Each painting includes:
| `checkup_checked` | Reviewed in checkup / debug workflow (gold frame in 3D when true) | | `checkup_checked` | Reviewed in checkup / debug workflow (gold frame in 3D when true) |
| `checkup_fixed` | Image replaced via **Fix it** | | `checkup_fixed` | Image replaced via **Fix it** |
Populate biographies with `npm run fetch-artist-bios` (see [data-and-images.md](data-and-images.md)). Populate biographies with `npm run dev: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). After `npm run import-painter-palette`, **`palette_metadata`** holds PainterPalette enrichment (nationality, styles, occupations, raw influence fields, etc.). Run `npm run migrate:artist-checkup-flags` and `npm run migrate:artist-palette` on existing databases. Artist objects also include `checkup_checked` and `checkup_fixed` (same semantics as paintings; gold portrait border when reviewed). After `npm run dev:import-painter-palette`, **`palette_metadata`** holds PainterPalette enrichment (nationality, styles, occupations, raw influence fields, etc.). Run `npm run dev:migrate:artist-checkup-flags` and `npm run dev:migrate:artist-palette` on existing databases.
--- ---
+16 -16
View File
@@ -1,12 +1,12 @@
# Art Gallery — database structure # Art Gallery — database structure
PostgreSQL schema for the virtual gallery. Canonical DDL lives in **`db/schema.sql`**; **`server/migrate.js`** (`npm run migrate`) applies that file plus idempotent incremental scripts in `db/migrate-*.sql`. This document describes the logical model. PostgreSQL schema for the virtual gallery. Canonical DDL lives in **`db/schema.sql`**; **`server/migrate.js`** (`npm run dev:migrate`) applies that file plus idempotent incremental scripts in `db/migrate-*.sql`. This document describes the logical model.
Connection settings come from `.env` (dev) or `infra/docker/.env.prod` (prod scripts). See [environments.md](environments.md) and [setup.md](setup.md). Connection settings come from `.env` (dev) or `infra/docker/.env.prod` (prod scripts). See [environments.md](environments.md) and [setup.md](setup.md).
### One-time split (legacy `Gallery` → `gallery_prod` + `gallery_dev`) ### One-time split (legacy `Gallery` → `gallery_prod` + `gallery_dev`)
Run [`db/split-dev-prod-pgadmin.sql`](../db/split-dev-prod-pgadmin.sql) in **pgAdmin** on the dev PC (postgres superuser). Alternative: `npm run db:split-databases` with `PGUSER=postgres`. Run [`db/split-dev-prod-pgadmin.sql`](../db/split-dev-prod-pgadmin.sql) in **pgAdmin** on the dev PC (postgres superuser). Alternative: `npm run infra:db:split-dev-prod` with `PGUSER=postgres`.
## Overview ## Overview
@@ -73,14 +73,14 @@ Finer-grained styles (Impressionism, Cubism, Suprematism, …).
| `birth_year`, `death_year` | INTEGER | Nullable; used for timeline portrait placement | | `birth_year`, `death_year` | INTEGER | Nullable; used for timeline portrait placement |
| `movement_id` | FK → `art_movements` | Primary movement | | `movement_id` | FK → `art_movements` | Primary movement |
| `portrait_path` | VARCHAR(500) | Relative to `data/images/`; nullable after debug **Clear** | | `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`) | | `bio_short`, `bio_full` | TEXT | Wikipedia lead section (`npm run dev:fetch-artist-bios`) |
| `wikipedia_title` | VARCHAR(300) | Source page title | | `wikipedia_title` | VARCHAR(300) | Source page title |
| `century` | INTEGER | Rounded century bucket for seeding limits | | `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_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 | | `checkup_fixed` | BOOLEAN NOT NULL DEFAULT false | Portrait replaced, cleared, or uploaded via debug |
| `palette_metadata` | JSONB | Enrichment from `Inputs/PainterPalette.csv` (`npm run import-painter-palette`) | | `palette_metadata` | JSONB | Enrichment from `Inputs/PainterPalette.csv` (`npm run dev:import-painter-palette`) |
Applied by `npm run migrate:artist-checkup-flags` (`db/migrate-artist-checkup-flags.sql`) and `npm run migrate:artist-palette` (`db/migrate-artist-palette.sql`). Applied by `npm run dev:migrate:artist-checkup-flags` (`db/migrate-artist-checkup-flags.sql`) and `npm run dev:migrate:artist-palette` (`db/migrate-artist-palette.sql`).
### `artist_periods` ### `artist_periods`
@@ -114,7 +114,7 @@ Phases within an artists career (e.g. “Blue Period”, “Roman Period”).
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. 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`). Applied by `npm run dev:migrate:checkup-flags` (`db/migrate-checkup-flags.sql`).
### `painting_annotations` ### `painting_annotations`
@@ -134,7 +134,7 @@ Short art-history notes shown on painting detail (`PaintingAnnotations.tsx`).
| `sort_order` | INTEGER | Display order within the painting | | `sort_order` | INTEGER | Display order within the painting |
| `confidence` | VARCHAR(20) | Default `curated`; Wikipedia pass uses `wikipedia` | | `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`). Applied by `npm run dev:migrate:painting-annotations` (`db/migrate-painting-annotations.sql`). Load data with `npm run dev:update-painting-annotations` (curated entries in `scripts/painting-annotations-data.js`; add `--wikipedia` for intro sentences from each works `wikipedia_title`).
### `painting_influences` ### `painting_influences`
@@ -154,7 +154,7 @@ Directed edges: *this painting* was influenced by *that painting*.
Unique constraint on `(painting_id, influenced_by_painting_id)`. Unique constraint on `(painting_id, influenced_by_painting_id)`.
**Legacy mirror table.** `npm run update-influences` still inserts painting-to-painting rows here when curating data. The same edges are copied into `painting_influence_sources`. The **REST API does not read this table** for painting detail or hall navigation — use `painting_influence_sources` as the source of truth for display. **Legacy mirror table.** `npm run dev:update-influences` still inserts painting-to-painting rows here when curating data. The same edges are copied into `painting_influence_sources`. The **REST API does not read this table** for painting detail or hall navigation — use `painting_influence_sources` as the source of truth for display.
### `painting_influence_sources` ### `painting_influence_sources`
@@ -196,7 +196,7 @@ Curator accounts (named logins). Anonymous site visitors do not have rows here.
| `created_at` | TIMESTAMPTZ | | | `created_at` | TIMESTAMPTZ | |
| `last_login_at` | TIMESTAMPTZ | Updated on successful login | | `last_login_at` | TIMESTAMPTZ | Updated on successful login |
First curator is bootstrapped on `npm run migrate` when `users` is empty and `CURATOR_USERNAME` / `CURATOR_PASSWORD` are set in env. First curator is bootstrapped on `npm run dev:migrate` when `users` is empty and `CURATOR_USERNAME` / `CURATOR_PASSWORD` are set in env.
### `curator_audit_log` ### `curator_audit_log`
@@ -240,7 +240,7 @@ PostgreSQL session store for `express-session` (`connect-pg-simple`). Not applic
## First-time setup ## First-time setup
The `gallery` database user needs `CREATE` on schema `public` for migrations. If tables cannot be created, run **`db/setup-admin.sql`** as PostgreSQL superuser (or the grants below) before `npm run migrate`: The `gallery` database user needs `CREATE` on schema `public` for migrations. If tables cannot be created, run **`db/setup-admin.sql`** as PostgreSQL superuser (or the grants below) before `npm run dev:migrate`:
```sql ```sql
GRANT CREATE ON SCHEMA public TO gallery; GRANT CREATE ON SCHEMA public TO gallery;
@@ -253,10 +253,10 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO gallery;
Then: Then:
```bash ```bash
npm run migrate npm run dev:migrate
npm run seed npm run dev:seed
npm run fetch-artist-bios npm run dev:fetch-artist-bios
npm run expand-catalog npm run dev:expand-catalog
``` ```
## Data conventions ## Data conventions
@@ -264,5 +264,5 @@ npm run expand-catalog
- **Year zero** is not used; BCE years are negative integers. - **Year zero** is not used; BCE years are negative integers.
- **Image paths** are relative to `IMAGE_DIR` (default `./data/images`), e.g. `portraits/Claude_Monet.jpg`, `paintings/thumbs/Raphael_The_School_of_Athens_thumb.jpg`. - **Image paths** are relative to `IMAGE_DIR` (default `./data/images`), e.g. `portraits/Claude_Monet.jpg`, `paintings/thumbs/Raphael_The_School_of_Athens_thumb.jpg`.
- **Seeding cap**: curated ingest targets at most ~100 artists per century to keep the catalog manageable. - **Seeding cap**: curated ingest targets at most ~100 artists per century to keep the catalog manageable.
- **Catalog expansion**: `scripts/famous-paintings-data.js` plus `npm run expand-catalog` raises thin artist catalogs to at least six notable paintings (`MIN_PAINTINGS`, default 6). - **Catalog expansion**: `scripts/famous-paintings-data.js` plus `npm run dev:expand-catalog` raises thin artist catalogs to at least six notable paintings (`MIN_PAINTINGS`, default 6).
- **Biographies**: `bio_short` and `bio_full` are populated by `npm run fetch-artist-bios` from English Wikipedia lead sections; `wikipedia_title` on the artist row is the source article. - **Biographies**: `bio_short` and `bio_full` are populated by `npm run dev:fetch-artist-bios` from English Wikipedia lead sections; `wikipedia_title` on the artist row is the source article.
+64 -62
View File
@@ -2,7 +2,9 @@
Quick cheat sheet for daily operations. All `npm` commands run from the **repository root** unless noted. Quick cheat sheet for daily operations. All `npm` commands run from the **repository root** unless noted.
**Default target: dev.** Day-to-day commands use `gallery_dev`, repo `data/images/`, and https://devgallery.mysuperlab.netcraze.pro. Prod (`gallery_prod`, Docker, `docker:publish`) is for **scheduled releases** (~weekly) — see [Promote dev → prod](#promote-dev--prod-scheduled-release) below. **Default target: dev.** Day-to-day commands use `gallery_dev`, repo `data/images/`, and https://devgallery.mysuperlab.netcraze.pro. Prod (`gallery_prod`, Docker, `prod:docker:publish`) is for **scheduled releases** (~weekly) — see [Promote dev → prod](#promote-dev--prod-scheduled-release) below.
**Script prefixes:** `dev:``gallery_dev` / dev PC · `prod:` → production / TrueNAS · `devtoprod:` → promote dev → prod · `prodto:dev:` → refresh dev from prod · `infra:` → one-time setup
**Environments:** **Environments:**
@@ -39,10 +41,10 @@ npm run dev:client # Vite on :5173
### Start — production-style (single Node process, built SPA) ### Start — production-style (single Node process, built SPA)
```powershell ```powershell
npm run start:prod # build client + serve on PORT from .env npm run prod:start # build client + serve on PORT from .env
# or: # or:
npm run build npm run prod:build
npm run start npm run dev:start
``` ```
### Stop dev servers ### Stop dev servers
@@ -68,7 +70,7 @@ Stop-Process -Id <PID> -Force
|--------|-------| |--------|-------|
| **Stop** | TrueNAS Web UI → Apps → **gallery-web** → Stop | | **Stop** | TrueNAS Web UI → Apps → **gallery-web** → Stop |
| **Restart** | Same → Restart (after `docker:publish` or config change) | | **Restart** | Same → Restart (after `docker:publish` or config change) |
| **Update image** | Dev PC: `npm run docker:publish` → restart app on TrueNAS | | **Update image** | Dev PC: `npm run prod:docker:publish` → restart app on TrueNAS |
--- ---
@@ -80,11 +82,11 @@ copy .env.example .env # edit DB credentials, PUBLIC_URL
npm install npm install
cd client; npm install; cd .. cd client; npm install; cd ..
npm run migrate # schema + incremental SQL (+ auth tables, bootstrap curator) npm run dev:migrate # schema + incremental SQL (+ auth tables, bootstrap curator)
npm run setup # migrate + seed (fresh empty DB only) npm run dev:setup # migrate + seed (fresh empty DB only)
``` ```
**Curator auth (after migrate):** set in `.env` before first `npm run migrate` if the DB has no curator yet: **Curator auth (after migrate):** set in `.env` before first `npm run dev:migrate` if the DB has no curator yet:
```env ```env
SESSION_SECRET=your-long-random-secret SESSION_SECRET=your-long-random-secret
@@ -120,7 +122,7 @@ CURATOR_USERNAME=curator
CURATOR_PASSWORD=your-secure-password CURATOR_PASSWORD=your-secure-password
``` ```
Run `npm run migrate` against prod DB after first deploy with auth vars set (creates tables + bootstrap curator if `users` is empty). Run `npm run dev:migrate` against prod DB after first deploy with auth vars set (creates tables + bootstrap curator if `users` is empty).
--- ---
@@ -128,14 +130,14 @@ Run `npm run migrate` against prod DB after first deploy with auth vars set (cre
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `npm run migrate` | Apply `db/schema.sql` + incremental migrations (safe to re-run) | | `npm run dev:migrate` | Apply `db/schema.sql` + incremental migrations (safe to re-run) |
| `npm run setup` | `migrate` + `seed` — fresh catalog from Wikipedia data | | `npm run dev:setup` | `migrate` + `seed` — fresh catalog from Wikipedia data |
| `npm run db:split-databases` | One-time: legacy `Gallery``gallery_prod` + `gallery_dev` (needs `PGUSER=postgres`) | | `npm run infra:db:split-dev-prod` | One-time: legacy `Gallery``gallery_prod` + `gallery_dev` (needs `PGUSER=postgres`) |
| `npm run db:sync-from-prod` | Clone `gallery_prod``gallery_dev` (TEMPLATE); also runs image sync from prod | | `npm run prodto:dev:db` | Clone `gallery_prod``gallery_dev` (TEMPLATE); also runs image sync from prod |
| `npm run db:backup` | Dev data-only backup → `db/DataBackup/*.txt` + `.zip` | | `npm run dev:db:backup` | Dev data-only backup → `db/DataBackup/*.txt` + `.zip` |
| `npm run db:backup:prod` | Prod backup (reads `infra/docker/.env.prod`) | | `npm run prod:db:backup` | Prod backup (reads `infra/docker/.env.prod`) |
| `npm run db:restore -- --file <path>` | Restore backup into **dev** (truncates tables first; prompts `yes`) | | `npm run dev:db:restore -- --file <path>` | Restore backup into **dev** (truncates tables first; prompts `yes`) |
| `npm run db:restore:prod -- --file <path>` | Restore into **prod** (requires confirmation) | | `npm run devtoprod:db:restore -- --file <path>` | Restore into **prod** (requires confirmation) |
**One-time split (recommended):** pgAdmin on dev PC → open [`db/split-dev-prod-pgadmin.sql`](../db/split-dev-prod-pgadmin.sql) → run each STEP on database `postgres`, then verify on `gallery_dev`. **One-time split (recommended):** pgAdmin on dev PC → open [`db/split-dev-prod-pgadmin.sql`](../db/split-dev-prod-pgadmin.sql) → run each STEP on database `postgres`, then verify on `gallery_dev`.
@@ -159,52 +161,52 @@ Run in this order on a **new** or **re-seeded** database:
| # | Command | What it does | | # | Command | What it does |
|---|---------|----------------| |---|---------|----------------|
| 1 | `npm run seed` | Eras, movements, artists, one flagship painting per artist | | 1 | `npm run dev:seed` | Eras, movements, artists, one flagship painting per artist |
| 2 | `npm run sync-image-paths` | Import paintings from `data/images/paintings/`; link paths in DB | | 2 | `npm run dev:sync-image-paths` | Import paintings from `data/images/paintings/`; link paths in DB |
| 3 | `npm run fetch-artist-images` | Portraits → `data/images/portraits/`, set `portrait_path` | | 3 | `npm run dev:fetch-artist-images` | Portraits → `data/images/portraits/`, set `portrait_path` |
| 4 | `npm run fetch-artist-bios` | Wikipedia bios → `bio_short` / `bio_full` | | 4 | `npm run dev:fetch-artist-bios` | Wikipedia bios → `bio_short` / `bio_full` |
| 5 | `npm run expand-catalog` | Add famous works per artist (below `MIN_PAINTINGS`) | | 5 | `npm run dev:expand-catalog` | Add famous works per artist (below `MIN_PAINTINGS`) |
| 6 | `npm run update-influences` | Influence graph (detail panels, 3D hall lamps) | | 6 | `npm run dev:update-influences` | Influence graph (detail panels, 3D hall lamps) |
| 7 | `npm run fetch-images -- --limit=50` | Download missing painting files (batch) | | 7 | `npm run dev:fetch-images -- --limit=50` | Download missing painting files (batch) |
**One-shot bootstrap:** `npm run setup` = steps 1 + migrate only; still run 27 for a full catalog. **One-shot bootstrap:** `npm run dev:setup` = steps 1 + migrate only; still run 27 for a full catalog.
### Useful flags ### Useful flags
```powershell ```powershell
npm run sync-image-paths -- --dry-run npm run dev:sync-image-paths -- --dry-run
npm run fetch-artist-images -- --force npm run dev:fetch-artist-images -- --force
npm run fetch-artist-bios -- --force npm run dev:fetch-artist-bios -- --force
npm run expand-catalog -- --fetch-images npm run dev:expand-catalog -- --fetch-images
npm run fetch-images -- --artist="Claude Monet" npm run dev:fetch-images -- --artist="Claude Monet"
npm run fetch-images -- --limit=50 --max-wait=120 npm run dev:fetch-images -- --limit=50 --max-wait=120
npm run update-influences -- --discover npm run dev:update-influences -- --discover
npm run discover-influences # discovery only, no curated insert npm run dev:discover-influences # discovery only, no curated insert
``` ```
### Optional migrations / imports ### Optional migrations / imports
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `npm run migrate:thumbnails` | Add thumbnail columns | | `npm run dev:migrate:thumbnails` | Add thumbnail columns |
| `npm run migrate:influence-sources` | `painting_influence_sources` table + backfill | | `npm run dev:migrate:influence-sources` | `painting_influence_sources` table + backfill |
| `npm run migrate:checkup-flags` | Review flags on `paintings` | | `npm run dev:migrate:checkup-flags` | Review flags on `paintings` |
| `npm run migrate:artist-checkup-flags` | Review flags on `artists` | | `npm run dev:migrate:artist-checkup-flags` | Review flags on `artists` |
| `npm run migrate:painting-annotations` | Art-history notes table | | `npm run dev:migrate:painting-annotations` | Art-history notes table |
| `npm run migrate:artist-palette` | `palette_metadata` JSONB on artists | | `npm run dev:migrate:artist-palette` | `palette_metadata` JSONB on artists |
| `npm run import-painter-palette` | Enrich from `Inputs/PainterPalette.csv` | | `npm run dev:import-painter-palette` | Enrich from `Inputs/PainterPalette.csv` |
| `npm run update-painting-annotations` | Load curated notes | | `npm run dev:update-painting-annotations` | Load curated notes |
| `npm run update-painting-annotations -- --wikipedia` | Add Wikipedia intro sentences | | `npm run dev:update-painting-annotations -- --wikipedia` | Add Wikipedia intro sentences |
### Audit / export ### Audit / export
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `npm run audit-painting-images` | Thumb vs full aspect-ratio mismatches | | `npm run dev:audit-painting-images` | Thumb vs full aspect-ratio mismatches |
| `npm run find-duplicates` | Duplicate / near-duplicate painting rows | | `npm run dev:find-duplicates` | Duplicate / near-duplicate painting rows |
| `npm run audit-influence-duplicates` | Duplicate influence edges | | `npm run dev:audit-influence-duplicates` | Duplicate influence edges |
| `npm run analyze-painter-palette` | CSV ↔ artist name match report | | `npm run dev:analyze-painter-palette` | CSV ↔ artist name match report |
| `npm run export-paintings` | Write `Output/paintings.csv` | | `npm run dev:export-paintings` | Write `Output/paintings.csv` |
--- ---
@@ -212,10 +214,10 @@ npm run discover-influences # discovery only, no curated insert
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `npm run fetch-images` | Search/download missing painting files (alias: `search-missing-paintings`) | | `npm run dev:fetch-images` | Search/download missing painting files (alias: `search-missing-paintings`) |
| `npm run fetch-artist-images` | Download or link artist portraits | | `npm run dev:fetch-artist-images` | Download or link artist portraits |
| `npm run sync-image-paths` | Align DB paths with files on disk; import new rows | | `npm run dev:sync-image-paths` | Align DB paths with files on disk; import new rows |
| `npm run regenerate-thumbnails` | Rebuild all thumbs from full images | | `npm run dev:regenerate-thumbnails` | Rebuild all thumbs from full images |
**Local paths:** `data/images/portraits/`, `data/images/paintings/`, `data/images/paintings/thumbs/` **Local paths:** `data/images/portraits/`, `data/images/paintings/`, `data/images/paintings/thumbs/`
@@ -228,8 +230,8 @@ SMB share **`Gallery`** → `/mnt/BasePool/Applications/Gallery` on TrueNAS.
```powershell ```powershell
net use \\192.168.10.122\Gallery /user:YOUR_TRUENAS_USER net use \\192.168.10.122\Gallery /user:YOUR_TRUENAS_USER
npm run images:sync-to-prod # dev repo → TrueNAS (promote / first deploy) npm run devtoprod:images # dev repo → TrueNAS (promote / first deploy)
npm run images:sync-from-prod # TrueNAS → dev repo npm run prodto:dev:images # TrueNAS → dev repo
``` ```
Type `yes` when prompted. Robocopy exit codes **07** = success. Type `yes` when prompted. Robocopy exit codes **07** = success.
@@ -240,13 +242,13 @@ Type `yes` when prompted. Robocopy exit codes **07** = success.
| Command | Where | Description | | Command | Where | Description |
|---------|-------|-------------| |---------|-------|-------------|
| `npm run docker:publish` | Dev PC, **Admin** PowerShell, Docker Desktop running | Build + push `gallery-web:latest` to Gitea | | `npm run prod:docker:publish` | Dev PC, **Admin** PowerShell, Docker Desktop running | Build + push `gallery-web:latest` to Gitea |
| `npm run docker:publish:push-only` | Same | Push only (skip rebuild) | | `npm run prod:docker:push-only` | Same | Push only (skip rebuild) |
| `.\infra\docker\save-for-truenas.ps1` | Dev PC | Save image to `gallery-web.tar` (offline deploy) | | `.\infra\docker\save-for-truenas.ps1` | Dev PC | Save image to `gallery-web.tar` (offline deploy) |
**Before first deploy:** `npm run docker:publish` → TrueNAS Custom App from `infra/docker/compose.truenas.yaml`. **Before first deploy:** `npm run prod:docker:publish` → TrueNAS Custom App from `infra/docker/compose.truenas.yaml`.
**After code changes:** `npm run docker:publish` → restart **gallery-web** on TrueNAS. **After code changes:** `npm run prod:docker:publish` → restart **gallery-web** on TrueNAS.
```powershell ```powershell
docker login gitea.mysuperlab.netcraze.pro docker login gitea.mysuperlab.netcraze.pro
@@ -257,7 +259,7 @@ docker login gitea.mysuperlab.netcraze.pro
## Build frontend ## Build frontend
```powershell ```powershell
npm run build # client → client/dist/ npm run prod:build # client → client/dist/
cd client && npm run build && cd .. cd client && npm run build && cd ..
``` ```
@@ -290,10 +292,10 @@ Expect JSON with `min_year` / `max_year`. HTML shell only from `curl` on `/` is
~Weekly (or when explicitly releasing to prod). Not part of daily dev. ~Weekly (or when explicitly releasing to prod). Not part of daily dev.
1. Test on https://devgallery.mysuperlab.netcraze.pro 1. Test on https://devgallery.mysuperlab.netcraze.pro
2. `npm run db:backup` 2. `npm run dev:db:backup`
3. `npm run db:restore:prod -- --file db/DataBackup/gallery_dev_data_....txt` (if DB/catalog changed) 3. `npm run devtoprod:db:restore -- --file db/DataBackup/gallery_dev_data_....txt` (if DB/catalog changed)
4. `npm run images:sync-to-prod` (if images changed) 4. `npm run devtoprod:images` (if images changed)
5. `npm run docker:publish` (if code changed) 5. `npm run prod:docker:publish` (if code changed)
6. Restart **gallery-web** on TrueNAS 6. Restart **gallery-web** on TrueNAS
7. Verify https://gallery.mysuperlab.netcraze.pro 7. Verify https://gallery.mysuperlab.netcraze.pro
+24 -11
View File
@@ -31,7 +31,8 @@ Gallery/
├── server/ # Express API, DB pool, image service ├── server/ # Express API, DB pool, image service
├── client/ # React/Vite frontend ├── client/ # React/Vite frontend
│ ├── src/ # Source (components, pages, 3D scene) │ ├── 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/GalleryWindows.tsx # Side-wall daylight windows (movement)
│ │ ├── components/HallPassage.tsx # Open archway between movement wings │ │ ├── components/HallPassage.tsx # Open archway between movement wings
│ │ ├── components/MovementHallDetails.tsx # Period architectural details │ │ ├── components/MovementHallDetails.tsx # Period architectural details
@@ -70,7 +71,7 @@ Gallery/
├── Output/ # Generated exports (e.g. paintings.csv) ├── Output/ # Generated exports (e.g. paintings.csv)
├── data/images/ # Local portraits and paintings (+ thumbs/) ├── data/images/ # Local portraits and paintings (+ thumbs/)
├── db/ # schema.sql, setup-admin.sql, migrate-*.sql ├── db/ # schema.sql, setup-admin.sql, migrate-*.sql
├── server/migrate.js # npm run migrate — schema + incremental migrations ├── server/migrate.js # npm run dev:migrate — schema + incremental migrations
├── deploy/ # Legacy nginx + systemd examples (optional) ├── deploy/ # Legacy nginx + systemd examples (optional)
├── infra/docker/ # Production Dockerfile, TrueNAS compose, deploy scripts ├── infra/docker/ # Production Dockerfile, TrueNAS compose, deploy scripts
├── Documentation/ # This folder ├── Documentation/ # This folder
@@ -92,12 +93,12 @@ Uses database **`gallery_dev`** on the same PostgreSQL host.
### Production (TrueNAS Docker) — scheduled releases ### Production (TrueNAS Docker) — scheduled releases
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 docker:publish` and the promote checklist in [environments.md](environments.md). 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) ### Production-style single process (local)
```bash ```bash
npm run start:prod # build client + serve on PORT from .env npm run prod:start # build client + serve on PORT from .env
``` ```
Serves `/api/*`, `/images/*`, and the built SPA from `client/dist`. Serves `/api/*`, `/images/*`, and the built SPA from `client/dist`.
@@ -159,7 +160,15 @@ On first visit, `HomePage.tsx` fetches the full catalog once:
2. `GET /api/timeline?start=…&end=…` — all eras and movements for that 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). 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. The “Loading art history…” message appears only until the first successful load completes. 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 |
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. 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.
@@ -196,7 +205,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 | | 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) | | 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 | | 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 (filtered client-side after initial load) | | 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 | | 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 ### Artists on movement streams
@@ -237,7 +246,7 @@ Each artist has **exactly one hall**. The hall is a rectangular room sized to fi
| Rule | Implementation | | Rule | Implementation |
|------|----------------| |------|----------------|
| One hall per artist | `VirtualGallery.tsx` builds a single room from that artists paintings | | 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 | | 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 **back, left, and right** walls in **one row per wall**; room **depth grows** when the catalog is large | | 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 | | 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 | | Wall order | On each wall, left → right: **later works on the left**, **earlier works on the right**; undated works sort toward the left |
@@ -265,7 +274,7 @@ Each artist has **exactly one hall**. The hall is a rectangular room sized to fi
| Click painting | Open detail view | | Click painting | Open detail view |
| Exit doorway / `E` / **Exit →** header button | Open path picker | | 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 ### Movement galleries
@@ -302,7 +311,9 @@ Movement galleries do **not** use the predecessor/successor influence picker —
### Shared 3D behaviour ### 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; 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 3D hall stays mounted while painting detail or bio overlays are open; returning remounts the canvas when the hall becomes active again. **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, and a **“Loading paintings…”** overlay is shown 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 ## Painting detail view
@@ -346,13 +357,15 @@ Next to the toggle, **Show more** (checkbox, persisted in `localStorage`) opens
- **Timeline bounds** derive from the earliest art movement start year, not ancient-era metadata alone, so the default view opens where catalogued content begins. - **Timeline bounds** derive from the earliest art movement start year, not ancient-era metadata alone, so the default view opens where catalogued content begins.
- **Timeline catalog** loads once from the API; pan/zoom is client-side only, with per-frame batching via `createViewChangeScheduler()`. - **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 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 only shows movements that have at least one artist active in the visible year range. - **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. - **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. - **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. - **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. - **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. The client calls `POST /api/artists/:id/preload-images` automatically when entering an **artist** hall (public route — links disk files only). - **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 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. - **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 ## User roles and access
@@ -404,7 +417,7 @@ 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**. **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 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.
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`. 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`.
+54 -54
View File
@@ -31,29 +31,29 @@ File names are sanitised `{Artist}_{Title}.{ext}`. The image service can redisco
| **Development** | `./data/images/` in repo | Working copy on dev PC | | **Development** | `./data/images/` in repo | Working copy on dev PC |
| **Production** | `/mnt/BasePool/Applications/Gallery/data/images` on TrueNAS | SMB `\\192.168.10.122\Gallery\data\images` | | **Production** | `/mnt/BasePool/Applications/Gallery/data/images` on TrueNAS | SMB `\\192.168.10.122\Gallery\data\images` |
Promote dev → prod files: `npm run images:sync-to-prod` (after `net use \\192.168.10.122\Gallery`). Refresh dev from prod: `npm run images:sync-from-prod`. See [environments.md](environments.md). Promote dev → prod files: `npm run devtoprod:images` (after `net use \\192.168.10.122\Gallery`). Refresh dev from prod: `npm run prodto:dev:images`. See [environments.md](environments.md).
## Scripts overview ## Scripts overview
| Script | npm command | Role | | Script | npm command | Role |
|--------|-------------|------| |--------|-------------|------|
| `seed-wikipedia.js` | `npm run seed` | Initial eras, movements, artists, one flagship painting per artist | | `seed-wikipedia.js` | `npm run dev:seed` | Initial eras, movements, artists, one flagship painting per artist |
| `seed-catalog-data.js` | *(data only)* | Eras, movements, artist metadata consumed by seed | | `seed-catalog-data.js` | *(data only)* | Eras, movements, artist metadata consumed by seed |
| `sync-image-paths.js` | `npm run sync-image-paths` | Import painting rows from disk; set `image_path` / `thumbnail_path` | | `sync-image-paths.js` | `npm run dev:sync-image-paths` | Import painting rows from disk; set `image_path` / `thumbnail_path` |
| `fetch-artist-images.js` | `npm run fetch-artist-images` | Download or link artist portraits under `data/images/portraits/` | | `fetch-artist-images.js` | `npm run dev:fetch-artist-images` | Download or link artist portraits under `data/images/portraits/` |
| `fetch-artist-bios.js` | `npm run fetch-artist-bios` | Wikipedia intros → `bio_short` / `bio_full` | | `fetch-artist-bios.js` | `npm run dev:fetch-artist-bios` | Wikipedia intros → `bio_short` / `bio_full` |
| `famous-paintings-data.js` | *(data only)* | Curated list of notable works per artist | | `famous-paintings-data.js` | *(data only)* | Curated list of notable works per artist |
| `expand-paintings.js` | `npm run expand-catalog` | Inserts works from data file for thin catalogs | | `expand-paintings.js` | `npm run dev:expand-catalog` | Inserts works from data file for thin catalogs |
| `art-influences-data.js` | *(data only)* | Curated influence edges (painting / artist / movement) | | `art-influences-data.js` | *(data only)* | Curated influence edges (painting / artist / movement) |
| `update-influences.js` | `npm run update-influences` | Applies influence graph; creates missing artists/works | | `update-influences.js` | `npm run dev:update-influences` | Applies influence graph; creates missing artists/works |
| `fetch-missing-images.js` | `npm run fetch-images` | Downloads files for paintings missing on disk | | `fetch-missing-images.js` | `npm run dev:fetch-images` | Downloads files for paintings missing on disk |
| `image-fetcher.js` | *(library)* | Wikimedia / museum resolution used by fetch scripts and API | | `image-fetcher.js` | *(library)* | Wikimedia / museum resolution used by fetch scripts and API |
| `sync-images-to-prod.ps1` / `sync-images-from-prod.ps1` | `npm run images:sync-*` | Robocopy via SMB `\\192.168.10.122\Gallery` | | `sync-images-to-prod.ps1` / `sync-images-from-prod.ps1` | `npm run devtoprod:images` / `npm run prodto:dev:images` | Robocopy via SMB `\\192.168.10.122\Gallery` |
| `regenerate-thumbnails.js` | `npm run regenerate-thumbnails` | Rebuild painting thumbs from full images via `sharp` | | `regenerate-thumbnails.js` | `npm run dev:regenerate-thumbnails` | Rebuild painting thumbs from full images via `sharp` |
| `regenerate-portrait-thumbs.js` | `npm run regenerate-portrait-thumbs` | Rebuild timeline portrait thumbs (~256px) and set `portrait_thumb_path` | | `regenerate-portrait-thumbs.js` | `npm run dev:regenerate-portrait-thumbs` | Rebuild timeline portrait thumbs (~256px) and set `portrait_thumb_path` |
| `audit-painting-images.js` | `npm run audit-painting-images` | Detect thumb/full aspect-ratio mismatches | | `audit-painting-images.js` | `npm run dev:audit-painting-images` | Detect thumb/full aspect-ratio mismatches |
| `find-duplicate-paintings.js` | `npm run find-duplicates` | Report exact and near-duplicate catalog rows | | `find-duplicate-paintings.js` | `npm run dev:find-duplicates` | Report exact and near-duplicate catalog rows |
| `migrate-checkup-flags.js` | `npm run migrate:checkup-flags` | Add `checkup_checked` / `checkup_fixed` columns | | `migrate-checkup-flags.js` | `npm run dev:migrate:checkup-flags` | Add `checkup_checked` / `checkup_fixed` columns |
## Typical workflow ## Typical workflow
@@ -71,13 +71,13 @@ migrate → seed → sync-image-paths → fetch-artist-images → fetch-artist-b
## Seeding pipeline ## Seeding pipeline
`npm run seed` runs `scripts/seed-wikipedia.js`, which: `npm run dev:seed` runs `scripts/seed-wikipedia.js`, which:
1. Inserts **historical eras** and **art movements** (curated date ranges and colours from `seed-catalog-data.js`). 1. Inserts **historical eras** and **art movements** (curated date ranges and colours from `seed-catalog-data.js`).
2. For each curated **artist**: 2. For each curated **artist**:
- Creates **artist periods** and one **flagship painting**. - Creates **artist periods** and one **flagship painting**.
- May download portraits and painting images when run with `--fetch-images`. - May download portraits and painting images when run with `--fetch-images`.
3. Does **not** insert influence edges — run `npm run update-influences` after seed (see [Painting influence graph](#painting-influence-graph)). 3. Does **not** insert influence edges — run `npm run dev:update-influences` after seed (see [Painting influence graph](#painting-influence-graph)).
Those influence edges power **3D hall navigation** and painting detail panels via **`painting_influence_sources`** (see `GET /api/artists/:id/navigation` and `GET /api/paintings/:id` in [API.md](API.md)). Those influence edges power **3D hall navigation** and painting detail panels via **`painting_influence_sources`** (see `GET /api/artists/:id/navigation` and `GET /api/paintings/:id` in [API.md](API.md)).
@@ -85,7 +85,7 @@ Artists are grouped by movement and century; the seed list targets at most ~100
## Artist biographies ## Artist biographies
`npm run fetch-artist-bios` reads each artists `wikipedia_title`, fetches the English Wikipedia **lead section**, and stores: `npm run dev:fetch-artist-bios` reads each artists `wikipedia_title`, fetches the English Wikipedia **lead section**, and stores:
| Field | Content | | Field | Content |
|-------|---------| |-------|---------|
@@ -110,15 +110,15 @@ The bio page (`ArtistBio.tsx`) shows lifespan, movement, summary, full text, and
## Expanding thin catalogs ## Expanding thin catalogs
Many seed artists arrive with only one famous painting. `npm run expand-catalog` runs `scripts/expand-paintings.js`, which: Many seed artists arrive with only one famous painting. `npm run dev:expand-catalog` runs `scripts/expand-paintings.js`, which:
1. Finds artists with fewer than `MIN_PAINTINGS` (default **6**). 1. Finds artists with fewer than `MIN_PAINTINGS` (default **6**).
2. Inserts rows from `scripts/famous-paintings-data.js` that are not already present (normalized title matching skips duplicates). 2. Inserts rows from `scripts/famous-paintings-data.js` that are not already present (normalized title matching skips duplicates).
3. Sets `wikipedia_title` on each new painting for image resolution. 3. Sets `wikipedia_title` on each new painting for image resolution.
```bash ```bash
npm run expand-catalog # DB rows only npm run dev:expand-catalog # DB rows only
npm run expand-catalog -- --fetch-images # also download images (very slow) npm run dev:expand-catalog -- --fetch-images # also download images (very slow)
``` ```
To add more works, append entries to `famous-paintings-data.js`: To add more works, append entries to `famous-paintings-data.js`:
@@ -137,7 +137,7 @@ Renaissance and medieval masters with large museum catalog dumps (e.g. Raphael,
When the repository includes a full `data/images/paintings/` tree but the database was seeded fresh (one row per artist), run: When the repository includes a full `data/images/paintings/` tree but the database was seeded fresh (one row per artist), run:
```bash ```bash
npm run sync-image-paths npm run dev:sync-image-paths
``` ```
`scripts/sync-image-paths.js`: `scripts/sync-image-paths.js`:
@@ -157,9 +157,9 @@ Typical result on a full clone: ~1,000+ paintings linked from ~1,000 on-disk fil
## Artist portraits ## Artist portraits
Timeline movement flow loads **`portrait_thumb_path`** (~256px JPEG under `portraits/thumbs/{Artist}_thumb.jpg`) when available; biography and 3D exit navigation use full `portrait_path`. After adding portraits, run `npm run regenerate-portrait-thumbs` to backfill thumbs on dev. Timeline movement flow loads **`portrait_thumb_path`** (~256px JPEG under `portraits/thumbs/{Artist}_thumb.jpg`) when available; biography and 3D exit navigation use full `portrait_path`. After adding portraits, run `npm run dev:regenerate-portrait-thumbs` to backfill thumbs on dev.
`npm run fetch-artist-images` runs `scripts/fetch-artist-images.js`: `npm run dev:fetch-artist-images` runs `scripts/fetch-artist-images.js`:
1. For each artist, checks `data/images/portraits/{Artist}.jpg` (or other extensions) and sets `portrait_path` when a local file exists. 1. For each artist, checks `data/images/portraits/{Artist}.jpg` (or other extensions) and sets `portrait_path` when a local file exists.
2. Otherwise downloads from Wikipedia / search fallbacks via `image-fetcher.js`. 2. Otherwise downloads from Wikipedia / search fallbacks via `image-fetcher.js`.
@@ -175,7 +175,7 @@ Run after seed when portrait files exist on disk but the DB still has null `port
Directed influence links are stored in **`painting_influence_sources`**. Each row connects a painting to a **source** of type `painting`, `artist`, or `movement`, with optional period context (e.g. influence during the works creation year). Directed influence links are stored in **`painting_influence_sources`**. Each row connects a painting to a **source** of type `painting`, `artist`, or `movement`, with optional period context (e.g. influence during the works creation year).
The legacy **`painting_influences`** table (painting-to-painting only) is still written alongside sources when running `npm run update-influences` — it keeps script compatibility and matches the backfill migration. **The API reads only `painting_influence_sources`**, so each edge appears once in the UI. The legacy **`painting_influences`** table (painting-to-painting only) is still written alongside sources when running `npm run dev:update-influences` — it keeps script compatibility and matches the backfill migration. **The API reads only `painting_influence_sources`**, so each edge appears once in the UI.
Influence data drives: Influence data drives:
@@ -188,7 +188,7 @@ Influence data drives:
Painting-to-painting edges exist in both tables by design. To confirm the database has no stray duplicates and that the API model is clean: Painting-to-painting edges exist in both tables by design. To confirm the database has no stray duplicates and that the API model is clean:
```bash ```bash
npm run audit-influence-duplicates npm run dev:audit-influence-duplicates
``` ```
Reports: edges present in both tables, duplicate rows within either table (should be 0), and legacy-only / sources-only mismatches. If *Influenced* ever shows the same successor twice, restart the server after pulling API fixes — responses must not union legacy and sources tables. Reports: edges present in both tables, duplicate rows within either table (should be 0), and legacy-only / sources-only mismatches. If *Influenced* ever shows the same successor twice, restart the server after pulling API fixes — responses must not union legacy and sources tables.
@@ -196,19 +196,19 @@ Reports: edges present in both tables, duplicate rows within either table (shoul
### One-time migration ### One-time migration
```bash ```bash
npm run migrate:influence-sources # create table + backfill legacy painting edges npm run dev:migrate:influence-sources # create table + backfill legacy painting edges
``` ```
### Curated updates ### Curated updates
`npm run update-influences` runs `scripts/update-influences.js` against `scripts/art-influences-data.js`: `npm run dev:update-influences` runs `scripts/update-influences.js` against `scripts/art-influences-data.js`:
```bash ```bash
npm run update-influences # insert curated edges npm run dev:update-influences # insert curated edges
npm run update-influences -- --fetch-images # also download images for newly created works npm run dev:update-influences -- --fetch-images # also download images for newly created works
npm run update-influences -- --discover # curated + web discovery pass npm run dev:update-influences -- --discover # curated + web discovery pass
npm run discover-influences # discovery only (no curated file pass) npm run dev:discover-influences # discovery only (no curated file pass)
npm run update-influences -- --discover --limit=20 # cap discovery to N works npm run dev:update-influences -- --discover --limit=20 # cap discovery to N works
``` ```
Each entry defines a later `work` and one or more `influencedBy` sources. Legacy single-object form is still supported: Each entry defines a later `work` and one or more `influencedBy` sources. Legacy single-object form is still supported:
@@ -290,14 +290,14 @@ Short curator-style notes on the painting detail page — separate from the infl
| UI | `PaintingAnnotations.tsx` — numbered markers on the image (when `pos_x` / `pos_y` set) plus an “Art history notes” list | | 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` | | API | Included as `annotations[]` on `GET /api/paintings/:id` |
| Curated data | `scripts/painting-annotations-data.js` — artist/title keys matched via `influence-resolver.js` | | 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) | | Load | `npm run dev: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 | | Wikipedia pass | `npm run dev: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. Categories include `subject`, `technique`, `context`, and `symbolism`. Sources cite Gombrich, museum catalogs, and Wikipedia as appropriate.
## Batch image fetch ## 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: `npm run dev:fetch-images` (alias: `npm run dev:search-missing-paintings`) runs `scripts/fetch-missing-images.js`. It searches multiple sources for paintings without local files:
| Source | Notes | | Source | Notes |
|--------|--------| |--------|--------|
@@ -315,13 +315,13 @@ Categories include `subject`, `technique`, `context`, and `symbolism`. Sources c
| Harvard Art Museums | Optional (`HARVARD_ART_API_KEY` in `.env`) | | Harvard Art Museums | Optional (`HARVARD_ART_API_KEY` in `.env`) |
```bash ```bash
npm run fetch-images # all missing, catalog order (~hours) npm run dev:fetch-images # all missing, catalog order (~hours)
npm run fetch-images -- --limit=50 # random sample of 50; 10s max per painting npm run dev:fetch-images -- --limit=50 # random sample of 50; 10s max per painting
npm run fetch-images -- --limit=250 # random sample up to N (caps at current missing count) npm run dev:fetch-images -- --limit=250 # random sample up to N (caps at current missing count)
npm run fetch-images -- --limit=50 --max-wait=120 # slower, more thorough lookup per painting npm run dev:fetch-images -- --limit=50 --max-wait=120 # slower, more thorough lookup per painting
npm run fetch-images -- --artist="Albrecht Dürer" # one artist, catalog order npm run dev:fetch-images -- --artist="Albrecht Dürer" # one artist, catalog order
npm run fetch-images -- --discover-only --limit=20 # fix wikipedia_title only npm run dev:fetch-images -- --discover-only --limit=20 # fix wikipedia_title only
npm run fetch-images -- --web-search-only --limit=50 # DuckDuckGo + Commons + multilingual Wikipedia npm run dev:fetch-images -- --web-search-only --limit=50 # DuckDuckGo + Commons + multilingual Wikipedia
``` ```
Each run prints **`Missing local files: N`** at startup — that is the current count of catalogued paintings with no full-size or thumbnail file under `data/images/`. A painting counts as present if **either** file exists on disk. Each run prints **`Missing local files: N`** at startup — that is the current count of catalogued paintings with no full-size or thumbnail file under `data/images/`. A painting counts as present if **either** file exists on disk.
@@ -377,10 +377,10 @@ These live in `client/public/` (and `client/dist/` after build).
1. Insert rows into `artists`, `artist_periods`, `paintings` (or extend the seed script). 1. Insert rows into `artists`, `artist_periods`, `paintings` (or extend the seed script).
2. Place image files under `data/images/` using the naming convention. 2. Place image files under `data/images/` using the naming convention.
3. Run `npm run fetch-artist-bios` for the new artists biography. 3. Run `npm run dev:fetch-artist-bios` for the new artists biography.
4. Add entries to `famous-paintings-data.js` and run `npm run expand-catalog` if needed. 4. Add entries to `famous-paintings-data.js` and run `npm run dev:expand-catalog` if needed.
5. Run `npm run fetch-images -- --artist="…"` or rely on preload / on-demand sync. 5. Run `npm run dev:fetch-images -- --artist="…"` or rely on preload / on-demand sync.
6. Add influence rows via `npm run update-influences` / `art-influences-data.js` (writes both `painting_influence_sources` and legacy painting edges). 6. Add influence rows via `npm run dev:update-influences` / `art-influences-data.js` (writes both `painting_influence_sources` and legacy painting edges).
## PainterPalette external dataset ## PainterPalette external dataset
@@ -389,8 +389,8 @@ These live in `client/public/` (and `client/dist/` after build).
### One-time setup ### One-time setup
```bash ```bash
npm run migrate:artist-palette # adds artists.palette_metadata JSONB npm run dev:migrate:artist-palette # adds artists.palette_metadata JSONB
npm run import-painter-palette # enrich + influence links npm run dev:import-painter-palette # enrich + influence links
``` ```
### What gets imported ### What gets imported
@@ -409,10 +409,10 @@ Name matching uses normalized strings plus aliases in `scripts/painter-palette-l
### Commands ### Commands
```bash ```bash
npm run analyze-painter-palette # match report npm run dev:analyze-painter-palette # match report
npm run import-painter-palette -- --dry-run npm run dev:import-painter-palette -- --dry-run
npm run import-painter-palette -- --metadata-only npm run dev:import-painter-palette -- --metadata-only
npm run import-painter-palette -- --influences-only npm run dev:import-painter-palette -- --influences-only
``` ```
Re-run `import-painter-palette` after adding gallery artists or updating the CSV; existing palette influence rows are skipped if already present. Re-run `import-painter-palette` after adding gallery artists or updating the CSV; existing palette influence rows are skipped if already present.
@@ -422,7 +422,7 @@ Re-run `import-painter-palette` after adding gallery artists or updating the CSV
Export the full painting catalog as CSV: Export the full painting catalog as CSV:
```bash ```bash
npm run export-paintings npm run dev:export-paintings
``` ```
Writes **`Output/paintings.csv`** with columns `artist`, `painting`, `year` (sorted by artist, year, title). The `Output/` folder is git-ignored by convention; regenerate after catalog changes. Writes **`Output/paintings.csv`** with columns `artist`, `painting`, `year` (sorted by artist, year, title). The `Output/` folder is git-ignored by convention; regenerate after catalog changes.
@@ -454,7 +454,7 @@ The catalog can contain the same work more than once — usually from a **double
### Find duplicates ### Find duplicates
```bash ```bash
npm run find-duplicates npm run dev:find-duplicates
``` ```
Runs `scripts/find-duplicate-paintings.js`, which reports: Runs `scripts/find-duplicate-paintings.js`, which reports:
+21 -21
View File
@@ -32,10 +32,10 @@ Unless a task explicitly says **prod** or **production**, assume the target is *
|------|---------|-----------|-----------|------| |------|---------|-----------|-----------|------|
| Stop dev servers | Dev PC `192.168.10.70` | PowerShell (normal) | your user | Close `dev:web` / `dev:server` / `dev:client` terminals | | Stop dev servers | Dev PC `192.168.10.70` | PowerShell (normal) | your user | Close `dev:web` / `dev:server` / `dev:client` terminals |
| **DB split (one-time)** | Dev PC | **pgAdmin** → Query Tool on `postgres` | **postgres superuser** | [`db/split-dev-prod-pgadmin.sql`](../db/split-dev-prod-pgadmin.sql) step by step | | **DB split (one-time)** | Dev PC | **pgAdmin** → Query Tool on `postgres` | **postgres superuser** | [`db/split-dev-prod-pgadmin.sql`](../db/split-dev-prod-pgadmin.sql) step by step |
| Dev migrate / dev:web | Dev PC | PowerShell (normal) | your user | `npm run migrate`, `npm run dev:web` | | Dev migrate / dev:web | Dev PC | PowerShell (normal) | your user | `npm run dev:migrate`, `npm run dev:web` |
| Image dir on TrueNAS | TrueNAS `192.168.10.122` | **Shell** (SSH or UI → System Settings → Shell) | root / sudo | `bash infra/docker/truenas-setup.sh` | | Image dir on TrueNAS | TrueNAS `192.168.10.122` | **Shell** (SSH or UI → System Settings → Shell) | root / sudo | `bash infra/docker/truenas-setup.sh` |
| Copy images to prod | Dev PC | PowerShell (normal) | SMB `\\192.168.10.122\Gallery` | `net use` then `npm run images:sync-to-prod` | | Copy images to prod | Dev PC | PowerShell (normal) | SMB `\\192.168.10.122\Gallery` | `net use` then `npm run devtoprod:images` |
| Build + push Docker image | Dev PC | **PowerShell as Administrator** | admin (for LAN hosts entry) | `npm run docker:publish` | | Build + push Docker image | Dev PC | **PowerShell as Administrator** | admin (for LAN hosts entry) | `npm run prod:docker:publish` |
| Install prod app | TrueNAS | **Web UI** → Apps → Custom App | admin | Paste `infra/docker/compose.truenas.yaml` | | Install prod app | TrueNAS | **Web UI** → Apps → Custom App | admin | Paste `infra/docker/compose.truenas.yaml` |
| Verify prod | Dev PC or TrueNAS | PowerShell / browser | any | `curl.exe -sk https://gallery.mysuperlab.netcraze.pro/api/bounds` | | Verify prod | Dev PC or TrueNAS | PowerShell / browser | any | `curl.exe -sk https://gallery.mysuperlab.netcraze.pro/api/bounds` |
@@ -94,7 +94,7 @@ psql -h 192.168.10.122 -U postgres -d postgres -f db/split-dev-prod.sql
```powershell ```powershell
cd C:\Users\SNAP\Nextcloud\Personal\Repo\Gallery cd C:\Users\SNAP\Nextcloud\Personal\Repo\Gallery
$env:PGHOST="192.168.10.122"; $env:PGUSER="postgres"; $env:PGPASSWORD="YOUR_POSTGRES_PASSWORD" $env:PGHOST="192.168.10.122"; $env:PGUSER="postgres"; $env:PGPASSWORD="YOUR_POSTGRES_PASSWORD"
npm run db:split-databases npm run infra:db:split-dev-prod
``` ```
### Step C — Configure dev PC and test ### Step C — Configure dev PC and test
@@ -113,13 +113,13 @@ npm run db:split-databases
CURATOR_PASSWORD=your-secure-password CURATOR_PASSWORD=your-secure-password
``` ```
`npm run migrate` creates auth tables and bootstraps the first curator when `users` is empty. `npm run dev:migrate` creates auth tables and bootstraps the first curator when `users` is empty.
2. Run: 2. Run:
```powershell ```powershell
cd C:\Users\SNAP\Nextcloud\Personal\Repo\Gallery cd C:\Users\SNAP\Nextcloud\Personal\Repo\Gallery
npm run migrate npm run dev:migrate
npm run dev:web npm run dev:web
``` ```
@@ -152,7 +152,7 @@ Requires SMB share **`Gallery`** → `/mnt/BasePool/Applications/Gallery` on Tru
net use \\192.168.10.122\Gallery /user:YOUR_TRUENAS_USER net use \\192.168.10.122\Gallery /user:YOUR_TRUENAS_USER
cd C:\Users\SNAP\Nextcloud\Personal\Repo\Gallery cd C:\Users\SNAP\Nextcloud\Personal\Repo\Gallery
npm run images:sync-to-prod npm run devtoprod:images
``` ```
UNC destination: `\\192.168.10.122\Gallery\data\images` UNC destination: `\\192.168.10.122\Gallery\data\images`
@@ -170,7 +170,7 @@ Prerequisites: **Docker Desktop running**, logged in to Gitea.
```powershell ```powershell
cd C:\Users\SNAP\Nextcloud\Personal\Repo\Gallery cd C:\Users\SNAP\Nextcloud\Personal\Repo\Gallery
docker login gitea.mysuperlab.netcraze.pro docker login gitea.mysuperlab.netcraze.pro
npm run docker:publish npm run prod:docker:publish
``` ```
Use normal PowerShell with `-SkipHosts` if you already added `192.168.10.122 gitea.mysuperlab.netcraze.pro` to `C:\Windows\System32\drivers\etc\hosts`. Use normal PowerShell with `-SkipHosts` if you already added `192.168.10.122 gitea.mysuperlab.netcraze.pro` to `C:\Windows\System32\drivers\etc\hosts`.
@@ -185,7 +185,7 @@ Use normal PowerShell with `-SkipHosts` if you already added `192.168.10.122 git
4. Replace `REPLACE_WITH_LONG_RANDOM_SECRET` and `REPLACE_WITH_SECURE_PASSWORD` for `SESSION_SECRET` and `CURATOR_PASSWORD` 4. Replace `REPLACE_WITH_LONG_RANDOM_SECRET` and `REPLACE_WITH_SECURE_PASSWORD` for `SESSION_SECRET` and `CURATOR_PASSWORD`
5. **Apps** → **Settings** → register Gitea registry (`gitea.mysuperlab.netcraze.pro`, token with `read:package`) 5. **Apps** → **Settings** → register Gitea registry (`gitea.mysuperlab.netcraze.pro`, token with `read:package`)
6. Deploy → wait for **gallery-web** to show **Running** 6. Deploy → wait for **gallery-web** to show **Running**
7. Run `npm run migrate` against `gallery_prod` if auth tables are not yet applied (or migrate from dev PC with prod env) 7. Run `npm run dev:migrate` against `gallery_prod` if auth tables are not yet applied (or migrate from dev PC with prod env)
### Step H — Verify production ### Step H — Verify production
@@ -298,8 +298,8 @@ See also [Drunkmeyou gitea-https-keenetic-npm-setup.md](../../Drunkmeyou/Documen
|------|---------| |------|---------|
| Public dev URL (Keenetic) | `npm run dev:web` | | Public dev URL (Keenetic) | `npm run dev:web` |
| Fast local HMR (no Keenetic) | `npm run dev:server` + `npm run dev:client` | | Fast local HMR (no Keenetic) | `npm run dev:server` + `npm run dev:client` |
| Refresh dev DB from prod | `npm run db:sync-from-prod` | | Refresh dev DB from prod | `npm run prodto:dev:db` |
| Pull prod images to dev | `npm run images:sync-from-prod` | | Pull prod images to dev | `npm run prodto:dev:images` |
--- ---
@@ -310,10 +310,10 @@ Run this when you are ready to ship dev to production — **not** after every sm
**Where:** Dev PC unless noted **Where:** Dev PC unless noted
1. Finish and test on https://devgallery.mysuperlab.netcraze.pro 1. Finish and test on https://devgallery.mysuperlab.netcraze.pro
2. `npm run db:backup` 2. `npm run dev:db:backup`
3. `npm run db:restore:prod -- --file db/DataBackup/gallery_dev_data_....txt` (type `yes`) — skip if only code changed and prod DB should stay as-is 3. `npm run devtoprod:db:restore -- --file db/DataBackup/gallery_dev_data_....txt` (type `yes`) — skip if only code changed and prod DB should stay as-is
4. `npm run images:sync-to-prod` — skip if no new/changed images 4. `npm run devtoprod:images` — skip if no new/changed images
5. `npm run docker:publish` — required when application code changed 5. `npm run prod:docker:publish` — required when application code changed
6. **TrueNAS Web UI** → restart **gallery-web** 6. **TrueNAS Web UI** → restart **gallery-web**
7. Verify https://gallery.mysuperlab.netcraze.pro 7. Verify https://gallery.mysuperlab.netcraze.pro
@@ -324,17 +324,17 @@ Run this when you are ready to ship dev to production — **not** after every sm
| Command | Where | Purpose | | Command | Where | Purpose |
|---------|-------|---------| |---------|-------|---------|
| pgAdmin + `split-dev-prod-pgadmin.sql` | Dev PC pgAdmin | One-time split (recommended) | | pgAdmin + `split-dev-prod-pgadmin.sql` | Dev PC pgAdmin | One-time split (recommended) |
| `npm run db:split-databases` | Dev PC PowerShell | Same split (needs `PGUSER=postgres`) | | `npm run infra:db:split-dev-prod` | Dev PC PowerShell | Same split (needs `PGUSER=postgres`) |
| `npm run db:sync-from-prod` | Dev PC PowerShell | Clone prod → dev | | `npm run prodto:dev:db` | Dev PC PowerShell | Clone prod → dev |
| `npm run db:backup` | Dev PC PowerShell | Dev backup | | `npm run dev:db:backup` | Dev PC PowerShell | Dev backup |
| `npm run db:restore:prod` | Dev PC PowerShell | Restore into prod | | `npm run devtoprod:db:restore` | Dev PC PowerShell | Restore into prod |
## Image sync ## Image sync
| Command | Where | Direction | | Command | Where | Direction |
|---------|-------|-----------| |---------|-------|-----------|
| `npm run images:sync-to-prod` | Dev PC PowerShell | Dev → TrueNAS volume | | `npm run devtoprod:images` | Dev PC PowerShell | Dev → TrueNAS volume |
| `npm run images:sync-from-prod` | Dev PC PowerShell | TrueNAS → dev repo | | `npm run prodto:dev:images` | Dev PC PowerShell | TrueNAS → dev repo |
## Safety guards ## Safety guards
+87 -89
View File
@@ -54,23 +54,23 @@ cd client && npm install && cd ..
One-shot setup (migrate + seed): One-shot setup (migrate + seed):
```bash ```bash
npm run setup npm run dev:setup
``` ```
Or step by step: Or step by step:
```bash ```bash
npm run migrate # db/schema.sql + db/migrate-*.sql via server/migrate.js npm run dev:migrate # db/schema.sql + db/migrate-*.sql via server/migrate.js
npm run seed # eras, movements, artists, flagship paintings (one per artist) npm run dev:seed # eras, movements, artists, flagship paintings (one per artist)
``` ```
`npm run migrate` is safe to re-run on existing databases (uses `IF NOT EXISTS` / `ADD COLUMN IF NOT EXISTS`). `npm run dev:migrate` is safe to re-run on existing databases (uses `IF NOT EXISTS` / `ADD COLUMN IF NOT EXISTS`).
If migration fails with permission errors, grant schema rights to the app user first (see [DB_structure.md](DB_structure.md)). If migration fails with permission errors, grant schema rights to the app user first (see [DB_structure.md](DB_structure.md)).
### Curator accounts (auth migration) ### Curator accounts (auth migration)
`npm run migrate` applies `db/migrate-auth.sql` (`users`, `curator_audit_log`, `session` tables). When the `users` table is empty and `CURATOR_USERNAME` / `CURATOR_PASSWORD` are set in `.env`, the first curator account is created automatically. `npm run dev:migrate` applies `db/migrate-auth.sql` (`users`, `curator_audit_log`, `session` tables). When the `users` table is empty and `CURATOR_USERNAME` / `CURATOR_PASSWORD` are set in `.env`, the first curator account is created automatically.
After migrate, sign in from the site header (**Curator login**). Debug mode, Checkup, and all mutating debug APIs require an active curator session. Anonymous visitors browse the timeline and 3D halls without logging in. After migrate, sign in from the site header (**Curator login**). Debug mode, Checkup, and all mutating debug APIs require an active curator session. Anonymous visitors browse the timeline and 3D halls without logging in.
@@ -81,50 +81,48 @@ See [API.md — Authentication](API.md#authentication) and [basics.md — Develo
After a fresh seed, run these to match a fully populated local install: After a fresh seed, run these to match a fully populated local install:
```bash ```bash
npm run sync-image-paths # import paintings from data/images/paintings/ (clone with image files) npm run dev:sync-image-paths # import paintings from data/images/paintings/ (clone with image files)
npm run fetch-artist-images # link local portraits or download from Wikipedia npm run dev:fetch-artist-images # link local portraits or download from Wikipedia
npm run fetch-artist-bios # bio_short / bio_full from Wikipedia npm run dev:fetch-artist-bios # bio_short / bio_full from Wikipedia
npm run expand-catalog # famous works for artists below MIN_PAINTINGS npm run dev:expand-catalog # famous works for artists below MIN_PAINTINGS
npm run update-influences # painting influence graph for detail view + hall exits npm run dev: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 dev: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 dev:migrate:artist-checkup-flags # optional: same flags for artist portraits (bio debug)
npm run migrate:painting-annotations # optional: art-history notes table npm run dev:migrate:painting-annotations # optional: art-history notes table
npm run update-painting-annotations # optional: load curated notes (+ --wikipedia for Wikipedia intros) npm run dev: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 dev: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 npm run dev:fetch-images -- --limit=50 --max-wait=120 # same batch size, longer lookup per work
cd client && npm run build && cd .. cd client && npm run build && cd ..
``` ```
**Order matters:** `sync-image-paths` should run when the repo already contains painting files under `data/images/paintings/` but the database only has one flagship work per artist (typical after `npm run setup` on a clone). `update-influences` is required for *Influenced By* / *Influenced* panels and golden lamps in the 3D hall — seed does not insert influence edges. **Order matters:** `dev:sync-image-paths` should run when the repo already contains painting files under `data/images/paintings/` but the database only has one flagship work per artist (typical after `npm run dev:setup` on a clone). `dev:update-influences` is required for *Influenced By* / *Influenced* panels and golden lamps in the 3D hall — seed does not insert influence edges.
Image fetch can take hours if you run it for the entire catalog. The first line of each run reports **`Missing local files: N`**. Use **`npm run fetch-images -- --limit=N`** for random batches (10s per painting by default), **`--artist="…"`** for one artist in catalog order, or on-demand resolution when viewing a painting in the detail view. Image fetch can take hours if you run it for the entire catalog. The first line of each run reports **`Missing local files: N`**. Use **`npm run dev:fetch-images -- --limit=N`** for random batches (10s per painting by default), **`--artist="…"`** for one artist in catalog order, or on-demand resolution when viewing a painting in the detail view.
## Run ## Run
| Command | Purpose | | Command | Purpose |
|---------|---------| |---------|---------|
| `npm run dev:web` | **Public dev stack** — Vite `:5173`, API `:3451` (Keenetic → devgallery…) | | `npm run dev:web` | **Public dev stack** — Vite `:5173`, API `:3451` (Keenetic → devgallery…) |
| `npm run docker:publish` | Build + push prod image to Gitea | | `npm run prod:docker:publish` | Build + push prod image to Gitea |
| `npm run images:sync-to-prod` | Copy `data/images/` → TrueNAS via SMB `Gallery` share | | `npm run devtoprod:images` | Copy `data/images/` → TrueNAS via SMB `Gallery` share |
| `npm run images:sync-from-prod` | Copy prod images → dev repo | | `npm run prodto:dev:images` | Copy prod images → dev repo |
| `npm run db:sync-from-prod` | Clone `gallery_prod``gallery_dev` | | `npm run prodto:dev:db` | Clone `gallery_prod``gallery_dev` |
| `npm run db:backup` / `db:restore:prod` | Dev backup / promote to prod | | `npm run dev:db:backup` / `devtoprod:db:restore` | Dev backup / promote DB to prod |
| `npm run build` | Build production SPA into `client/dist` | | `npm run prod:build` | Build production SPA into `client/dist` |
| `npm run start` | API + static SPA on `HOST`:`PORT` | | `npm run dev:start` | API + static SPA on `HOST`:`PORT` (uses root `.env`) |
| `npm run start:prod` | Build client, then start server | | `npm run prod:start` | Build client, then start server |
| `npm run server` | Alias for `start` | | `npm run dev:server` | API with nodemon reload (local HMR workflow) |
| `npm run dev:server` | API with nodemon reload (local `:3520` / `:5173` workflow) |
| `npm run dev:client` | Vite dev server on :5173 | | `npm run dev:client` | Vite dev server on :5173 |
| `npm run dev` | Alias for `start` |
See [environments.md](environments.md) for dev/prod URLs, database split, Docker deploy, and sync commands. Quick reference: [FAC.md](FAC.md). See [environments.md](environments.md) for dev/prod URLs, database split, Docker deploy, and sync commands. Quick reference: [FAC.md](FAC.md).
**Production frontend:** build the client, then start the server: **Production frontend:** build the client, then start the server:
```bash ```bash
npm run build npm run prod:build
# or: cd client && npm run build && cd .. # or: cd client && npm run build && cd ..
npm run start npm run dev:start
``` ```
Open http://localhost:5173 (Vite) or http://localhost:3451 (API only). Open http://localhost:5173 (Vite) or http://localhost:3451 (API only).
@@ -144,9 +142,9 @@ Quick deploy checklist:
1. One-time DB split in **pgAdmin** on dev PC: [`db/split-dev-prod-pgadmin.sql`](../db/split-dev-prod-pgadmin.sql) 1. One-time DB split in **pgAdmin** on dev PC: [`db/split-dev-prod-pgadmin.sql`](../db/split-dev-prod-pgadmin.sql)
2. Dev `.env``DB_NAME=gallery_dev`, `PORT=3451`, `PUBLIC_URL=https://devgallery.mysuperlab.netcraze.pro`, plus auth vars (`SESSION_SECRET`, `CURATOR_*`) 2. Dev `.env``DB_NAME=gallery_dev`, `PORT=3451`, `PUBLIC_URL=https://devgallery.mysuperlab.netcraze.pro`, plus auth vars (`SESSION_SECRET`, `CURATOR_*`)
3. `npm run migrate` on dev and prod DBs (includes auth tables + bootstrap curator) 3. `npm run dev:migrate` on dev and prod DBs (includes auth tables + bootstrap curator)
4. `net use \\192.168.10.122\Gallery``npm run images:sync-to-prod` 4. `net use \\192.168.10.122\Gallery``npm run devtoprod:images`
5. `npm run docker:publish` → TrueNAS Custom App from `infra/docker/compose.truenas.yaml` (set auth env in compose) 5. `npm run prod:docker:publish` → TrueNAS Custom App from `infra/docker/compose.truenas.yaml` (set auth env in compose)
6. Keenetic: both domains → `:5173`, protocol to device **`http`**, correct IP per environment 6. Keenetic: both domains → `:5173`, protocol to device **`http`**, correct IP per environment
### Legacy deployment (optional) ### Legacy deployment (optional)
@@ -157,48 +155,48 @@ Node on the dev PC at `:3520` with nginx → Vite `:5173` is superseded by TrueN
| Command | Script | Purpose | | Command | Script | Purpose |
|---------|--------|---------| |---------|--------|---------|
| `npm run seed` | `scripts/seed-wikipedia.js` | Reload curated Wikipedia data | | `npm run dev:seed` | `scripts/seed-wikipedia.js` | Reload curated Wikipedia data |
| `npm run fetch-artist-bios` | `scripts/fetch-artist-bios.js` | Wikipedia lead sections → `bio_short` / `bio_full` | | `npm run dev:fetch-artist-bios` | `scripts/fetch-artist-bios.js` | Wikipedia lead sections → `bio_short` / `bio_full` |
| `npm run fetch-artist-bios -- --force` | ↑ | Refresh bios even when already set | | `npm run dev:fetch-artist-bios -- --force` | ↑ | Refresh bios even when already set |
| `npm run expand-catalog` | `scripts/expand-paintings.js` | Insert famous works from `famous-paintings-data.js` | | `npm run dev:expand-catalog` | `scripts/expand-paintings.js` | Insert famous works from `famous-paintings-data.js` |
| `npm run expand-catalog -- --fetch-images` | ↑ | Also download images for new rows (slow) | | `npm run dev:expand-catalog -- --fetch-images` | ↑ | Also download images for new rows (slow) |
| `npm run fetch-images` | `scripts/fetch-missing-images.js` | Search Wikipedia, Commons, museums for missing files | | `npm run dev:fetch-images` | `scripts/fetch-missing-images.js` | Search Wikipedia, Commons, museums for missing files |
| `npm run search-missing-paintings` | ↑ (alias) | Same as `fetch-images` | | `npm run dev:search-missing-paintings` | ↑ (alias) | Same as `fetch-images` |
| `npm run fetch-images -- --artist="Name"` | ↑ | Limit to one artist (catalog order) | | `npm run dev:fetch-images -- --artist="Name"` | ↑ | Limit to one artist (catalog order) |
| `npm run fetch-images -- --limit=50` | ↑ | Random sample of N missing paintings | | `npm run dev:fetch-images -- --limit=50` | ↑ | Random sample of N missing paintings |
| `npm run fetch-images -- --limit=250` | ↑ | Larger random batch (processes min(N, missing count)) | | `npm run dev:fetch-images -- --limit=250` | ↑ | Larger random batch (processes min(N, missing count)) |
| `npm run fetch-images -- --limit=50 --max-wait=120` | ↑ | Random batch; 120s cap per painting | | `npm run dev:fetch-images -- --limit=50 --max-wait=120` | ↑ | Random batch; 120s cap per painting |
| `npm run fetch-images -- --discover-only --limit=20` | ↑ | Fix `wikipedia_title` via search only | | `npm run dev:fetch-images -- --discover-only --limit=20` | ↑ | Fix `wikipedia_title` via search only |
| `npm run regenerate-thumbnails` | `scripts/regenerate-thumbnails.js` | Rebuild all thumbs from full local files | | `npm run dev:regenerate-thumbnails` | `scripts/regenerate-thumbnails.js` | Rebuild all thumbs from full local files |
| `npm run audit-painting-images` | `scripts/audit-painting-images.js` | List thumb/full aspect-ratio mismatches | | `npm run dev:audit-painting-images` | `scripts/audit-painting-images.js` | List thumb/full aspect-ratio mismatches |
| `npm run migrate:thumbnails` | `db/migrate-thumbnails.sql` | Add thumbnail columns | | `npm run dev:migrate:thumbnails` | `db/migrate-thumbnails.sql` | Add thumbnail columns |
| `npm run fetch-artist-images` | `scripts/fetch-artist-images.js` | Download portraits or link existing files under `data/images/portraits/` | | `npm run dev:fetch-artist-images` | `scripts/fetch-artist-images.js` | Download portraits or link existing files under `data/images/portraits/` |
| `npm run fetch-artist-images -- --force` | ↑ | Re-fetch even when `portrait_path` is set | | `npm run dev:fetch-artist-images -- --force` | ↑ | Re-fetch even when `portrait_path` is set |
| `npm run sync-image-paths` | `scripts/sync-image-paths.js` | Link `image_path` / `thumbnail_path` and import missing painting rows from disk | | `npm run dev:sync-image-paths` | `scripts/sync-image-paths.js` | Link `image_path` / `thumbnail_path` and import missing painting rows from disk |
| `npm run sync-image-paths -- --dry-run` | ↑ | Report only, no DB writes | | `npm run dev:sync-image-paths -- --dry-run` | ↑ | Report only, no DB writes |
| `npm run migrate:influence-sources` | `scripts/migrate-influence-sources.js` | Create `painting_influence_sources` + backfill legacy edges | | `npm run dev: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 dev: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 dev: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 dev:migrate:painting-annotations` | `scripts/migrate-painting-annotations.js` | Create `painting_annotations` table |
| `npm run migrate:artist-palette` | `scripts/migrate-artist-palette.js` | Add `palette_metadata` JSONB on `artists` | | `npm run dev:migrate:artist-palette` | `scripts/migrate-artist-palette.js` | Add `palette_metadata` JSONB on `artists` |
| `npm run import-painter-palette` | `scripts/import-painter-palette.js` | Enrich artists + influence links from `Inputs/PainterPalette.csv` | | `npm run dev:import-painter-palette` | `scripts/import-painter-palette.js` | Enrich artists + influence links from `Inputs/PainterPalette.csv` |
| `npm run analyze-painter-palette` | `scripts/analyze-painter-palette.js` | Report CSV ↔ gallery artist name matches | | `npm run dev:analyze-painter-palette` | `scripts/analyze-painter-palette.js` | Report CSV ↔ gallery artist name matches |
| `npm run export-paintings` | `scripts/export-paintings-csv.js` | Write `Output/paintings.csv` (artist, painting, year) | | `npm run dev:export-paintings` | `scripts/export-paintings-csv.js` | Write `Output/paintings.csv` (artist, painting, year) |
| `npm run update-painting-annotations` | `scripts/update-painting-annotations.js` | Load curated notes from `painting-annotations-data.js` | | `npm run dev: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 dev: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 dev: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 dev:find-duplicates` | `scripts/find-duplicate-paintings.js` | Report duplicate and near-duplicate painting rows |
| `npm run audit-influence-duplicates` | `scripts/audit-influence-duplicates.js` | Report mirrored legacy/sources edges and duplicate influence rows | | `npm run dev:audit-influence-duplicates` | `scripts/audit-influence-duplicates.js` | Report mirrored legacy/sources edges and duplicate influence rows |
| `npm run update-influences` | `scripts/update-influences.js` | Insert influence links (painting / artist / movement) from `art-influences-data.js` | | `npm run dev: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 | | `npm run dev:update-influences -- --fetch-images` | ↑ | Also download images for newly created works |
| `npm run update-influences -- --discover` | ↑ | Curated pass + web discovery (Wikipedia, Wikidata, Met, art-history sites) | | `npm run dev:update-influences -- --discover` | ↑ | Curated pass + web discovery (Wikipedia, Wikidata, Met, art-history sites) |
| `npm run discover-influences` | `update-influences.js --discover-only` | Discovery pass only | | `npm run dev:discover-influences` | `update-influences.js --discover-only` | Discovery pass only |
### Scripts in the repository ### Scripts in the repository
These are checked in and maintained: These are checked in and maintained:
- `seed-wikipedia.js`, `seed-catalog-data.js` — initial catalog (`npm run seed`) - `seed-wikipedia.js`, `seed-catalog-data.js` — initial catalog (`npm run dev:seed`)
- `image-fetcher.js` — Wikimedia / museum image resolution - `image-fetcher.js` — Wikimedia / museum image resolution
- `fetch-artist-images.js` — artist portrait download / disk linking - `fetch-artist-images.js` — artist portrait download / disk linking
- `sync-image-paths.js` — import paintings and align paths from `data/images/paintings/` - `sync-image-paths.js` — import paintings and align paths from `data/images/paintings/`
@@ -232,44 +230,44 @@ After clone: copy `.env.example` → `.env`, install dependencies, run [one-time
| Symptom | Likely cause | Fix | | Symptom | Likely cause | Fix |
|---------|--------------|-----| |---------|--------------|-----|
| Empty timeline | DB not seeded | `npm run seed` | | Empty timeline | DB not seeded | `npm run dev:seed` |
| 502 / 504 on public URL | Keenetic rule wrong (IP, port, or `https` to device) | Dev → `192.168.10.70:5173`; prod → `192.168.10.122:5173`; protocol **`http`** — see [environments.md](environments.md) | | 502 / 504 on public URL | Keenetic rule wrong (IP, port, or `https` to device) | Dev → `192.168.10.70:5173`; prod → `192.168.10.122:5173`; protocol **`http`** — see [environments.md](environments.md) |
| 503 on public URL | Dev servers not running | `npm run dev:web` (or `dev:server` + `dev:client` for local HMR) | | 503 on public URL | Dev servers not running | `npm run dev:web` (or `dev:server` + `dev:client` for local HMR) |
| **`manifest unknown`** on TrueNAS deploy | Image not in Gitea | `npm run docker:publish` on dev PC first | | **`manifest unknown`** on TrueNAS deploy | Image not in Gitea | `npm run prod:docker:publish` on dev PC first |
| 500 on all `/api/*` | Wrong `.env` or Postgres down | Check connection, logs | | 500 on all `/api/*` | Wrong `.env` or Postgres down | Check connection, logs |
| “Biographical information not yet available” | Bios not fetched | `npm run fetch-artist-bios` | | “Biographical information not yet available” | Bios not fetched | `npm run dev:fetch-artist-bios` |
| Placeholder portraits on timeline | `portrait_path` not set | `npm run fetch-artist-images` | | Placeholder portraits on timeline | `portrait_path` not set | `npm run dev:fetch-artist-images` |
| Artist hall has only 12 paintings | DB not expanded / disk not imported | `npm run sync-image-paths` then `npm run expand-catalog`; extend `famous-paintings-data.js` | | Artist hall has only 12 paintings | DB not expanded / disk not imported | `npm run dev:sync-image-paths` then `npm run dev:expand-catalog`; extend `famous-paintings-data.js` |
| Movement flow shows “Loading art history…” repeatedly | Stale client refetching on every pan/zoom | Pull latest client; timeline loads catalog once — hard-refresh | | Movement flow shows “Loading art history…” repeatedly | Stale client refetching on every pan/zoom | Pull latest client; timeline loads catalog once — hard-refresh |
| Movement flow slow on first load | Large artist payload | Client uses `GET /api/artists?timeline=1` (no full bios); rebuild client | | Movement flow slow on first load | Large artist payload | Client uses `GET /api/artists?timeline=1` (no full bios); rebuild client |
| Movement flow zoom sluggish / dead over portraits | Stale client build | Rebuild client — wheel uses capture listener + interaction-mode rendering; hard-refresh | | Movement flow zoom sluggish / dead over portraits | Stale client build | Rebuild client — wheel uses capture listener + interaction-mode rendering; hard-refresh |
| Timeline year labels overlap when zoomed out | Stale client | Rebuild client — `chooseTimelineTickInterval()` adapts step to span and bar width | | Timeline year labels overlap when zoomed out | Stale client | Rebuild client — `chooseTimelineTickInterval()` adapts step to span and bar width |
| Black frames / canvas covers in 3D gallery | No local image for painting | `npm run fetch-images -- --limit=50` or `--artist="…"`; then `POST …/preload-images` | | Black frames / canvas covers in 3D gallery | No local image for painting | `npm run dev:fetch-images -- --limit=50` or `--artist="…"`; then `POST …/preload-images` |
| Many `⏱ timeout` lines in fetch batch | Default 10s cap too short for hard works | `--max-wait=120` or raise `FETCH_MAX_WAIT_SEC` | | Many `⏱ timeout` lines in fetch batch | Default 10s cap too short for hard works | `--max-wait=120` or raise `FETCH_MAX_WAIT_SEC` |
| White/grey flicker on frames | Texture loading or z-fighting with wall | Rebuild client (`cd client && npm run build`); ensure latest `VirtualGallery.tsx` | | White/grey flicker on frames | Texture loading or z-fighting with wall | Rebuild client (`cd client && npm run build`); ensure latest `VirtualGallery.tsx` |
| Wrong painting in 3D gallery frame | Stale or mismatched thumbnail file | `npm run regenerate-thumbnails`; gallery prefers full `image_path` | | Wrong painting in 3D gallery frame | Stale or mismatched thumbnail file | `npm run dev:regenerate-thumbnails`; gallery prefers full `image_path` |
| Wrong painting image (fetch) | Bad museum / search match on first download | Add entry to `DIRECT_IMAGE_OVERRIDES` in `scripts/image-fetcher.js`, re-fetch file | | Wrong painting image (fetch) | Bad museum / search match on first download | Add entry to `DIRECT_IMAGE_OVERRIDES` in `scripts/image-fetcher.js`, re-fetch file |
| Empty exit navigation lists | No influence edges for artist | `npm run update-influences`; check `painting_influence_sources` | | Empty exit navigation lists | No influence edges for artist | `npm run dev:update-influences`; check `painting_influence_sources` |
| Empty *Influenced By* / *Influenced* on painting detail | No edges for that work | `npm run migrate:influence-sources` then `npm run update-influences`; extend `art-influences-data.js` or run `--discover` | | Empty *Influenced By* / *Influenced* on painting detail | No edges for that work | `npm run dev:migrate:influence-sources` then `npm run dev:update-influences`; extend `art-influences-data.js` or run `--discover` |
| Same work listed twice under *Influenced* | Stale server merging legacy + sources tables | Restart server; API reads `painting_influence_sources` only — run `npm run audit-influence-duplicates` to verify DB | | Same work listed twice under *Influenced* | Stale server merging legacy + sources tables | Restart server; API reads `painting_influence_sources` only — run `npm run dev:audit-influence-duplicates` to verify DB |
| No golden lamps above frames in 3D hall | Stale API process or no influence edges | Restart server after API changes; run `npm run update-influences` | | No golden lamps above frames in 3D hall | Stale API process or no influence edges | Restart server after API changes; run `npm run dev:update-influences` |
| Default Vite page instead of gallery | `client/dist` missing or stale | `cd client && npm run build` | | Default Vite page instead of gallery | `client/dist` missing or stale | `cd client && npm run build` |
| Permission denied creating tables | `gallery` user lacks CREATE | Run admin grants, then migrate | | 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 | | 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 | | Checkup **Reviewed** toggle returns 404 | Stale server process missing new routes | Restart `npm run dev:web` or `npm run dev:server` after pulling API changes |
| Debug **More** / **Clear** / **Upload** / **Remove entry** returns 404 | Stale server process | Restart `npm run start` or `npm run dev:server`; routes in `server/index.js` + `server/image-service.js` | | Debug **More** / **Clear** / **Upload** / **Remove entry** returns 404 | Stale server process | Restart `npm run dev:start` or `npm run dev:server`; routes in `server/index.js` + `server/image-service.js` |
| Debug **Remove entry** — button stuck or missing on next painting | Stale client build | `cd client && npm run build`; hard-refresh — detail view remounts per painting id | | Debug **Remove entry** — button stuck or missing on next painting | Stale client build | `cd client && npm run build`; hard-refresh — detail view remounts per painting id |
| 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 | | 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` | | No art-history notes on painting detail | Annotations not migrated or loaded | `npm run dev:migrate:painting-annotations` then `npm run dev: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 | | **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) | | 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 | | 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 | | Duplicate works in gallery / timeline | Double import or variant Wikipedia titles | `npm run dev: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 | | **Failed to load movement gallery** / `Cannot GET /api/movements/:id/gallery` | Stale server process missing route | Restart `npm run dev:web` 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 | | 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 | | Windows overlap paintings in movement wing | Stale client | Rebuild client — windows are placed only on side walls in gaps between frames |
| Influence thumbnails cropped on painting detail | Stale client build | `npm run build` — panels use `object-fit: contain` for full image | | Influence thumbnails cropped on painting detail | Stale client build | `npm run prod:build` — panels use `object-fit: contain` for full image |
| **Curator login** fails / always guest | Auth tables missing or wrong password | Set `SESSION_SECRET` + `CURATOR_PASSWORD` in `.env`, run `npm run migrate`, restart server | | **Curator login** fails / always guest | Auth tables missing or wrong password | Set `SESSION_SECRET` + `CURATOR_PASSWORD` in `.env`, run `npm run dev:migrate`, restart server |
| Debug / Checkup returns **401** | Not signed in as curator | **Curator login** (top-right); session cookie `gallery.sid` must be sent (`credentials: include`) | | Debug / Checkup returns **401** | Not signed in as curator | **Curator login** (top-right); session cookie `gallery.sid` must be sent (`credentials: include`) |
| Debug works in UI but API rejects | Stale server without auth middleware | Restart `npm run dev:web` or `npm run dev:server` after pulling auth changes | | Debug works in UI but API rejects | Stale server without auth middleware | Restart `npm run dev:web` or `npm run dev:server` after pulling auth changes |
| **Empty screen** entering 3D hall (header missing) | Stale client before gallery-session fix | Hard-refresh; pull latest client — hall renders from `view` state, not only `gallerySession` | | **Empty screen** entering 3D hall (header missing) | Stale client before gallery-session fix | Hard-refresh; pull latest client — hall renders from `view` state, not only `gallerySession` |
+15 -15
View File
@@ -32,26 +32,26 @@ Interactive virtual art gallery: zoomable historical timeline with era click-to-
3. Apply database schema (requires PostgreSQL superuser for first-time setup): 3. Apply database schema (requires PostgreSQL superuser for first-time setup):
```bash ```bash
npm run migrate npm run dev:migrate
npm run seed npm run dev:seed
``` ```
4. Enrich the catalog (recommended after seed): 4. Enrich the catalog (recommended after seed):
```bash ```bash
npm run sync-image-paths # import paintings from data/images/paintings/ when present npm run dev:sync-image-paths # import paintings from data/images/paintings/ when present
npm run fetch-artist-images # link or download artist portraits npm run dev:fetch-artist-images # link or download artist portraits
npm run fetch-artist-bios # Wikipedia biographies for all artists npm run dev:fetch-artist-bios # Wikipedia biographies for all artists
npm run expand-catalog # add famous works for artists with thin catalogs npm run dev:expand-catalog # add famous works for artists with thin catalogs
npm run update-influences # art-history lineage links (detail panels + 3D hall) npm run dev:update-influences # art-history lineage links (detail panels + 3D hall)
npm run migrate:artist-palette # optional: JSONB column for PainterPalette enrichment npm run dev:migrate:artist-palette # optional: JSONB column for PainterPalette enrichment
npm run import-painter-palette # optional: metadata + influence links from Inputs/PainterPalette.csv npm run dev:import-painter-palette # optional: metadata + influence links from Inputs/PainterPalette.csv
npm run migrate:checkup-flags # review/fixed flags for Checkup + debug mode (paintings) npm run dev:migrate:checkup-flags # review/fixed flags for Checkup + debug mode (paintings)
npm run migrate:artist-checkup-flags # same flags for artist portraits (bio debug) npm run dev:migrate:artist-checkup-flags # same flags for artist portraits (bio debug)
npm run migrate:painting-annotations # art-history notes on painting detail npm run dev:migrate:painting-annotations # art-history notes on painting detail
npm run update-painting-annotations # load curated notes (+ optional --wikipedia) npm run dev:update-painting-annotations # load curated notes (+ optional --wikipedia)
npm run fetch-images -- --limit=50 # random batch of missing images (10s per work) npm run dev:fetch-images -- --limit=50 # random batch of missing images (10s per work)
npm run fetch-images -- --artist="Claude Monet" # one artist in catalog order npm run dev:fetch-images -- --artist="Claude Monet" # one artist in catalog order
``` ```
5. **Development (public URL):** 5. **Development (public URL):**