`package.json` has had a `test` script since the interior-polish work landed, and 12 files under `server/__tests__/` now guard hall geometry, clearance, brightness, window styles and the movement-links validators. Nothing outside `data-and-images.md` said the suite existed, and no document said how to run it — despite Bible.md requiring tests to be written before the code. FAC.md gains a Tests section between Verify and Build frontend: the command, what the suite covers by area, and the two properties that make it cheap to run — no test framework (Node's built-in runner) and no database or server, since the tests read the client's style and geometry modules from disk. setup.md lists `npm test` first in the Run table and notes on the Node prerequisite that the script depends on `node --test` expanding `**` itself. README.md gets the command beside the dev-server ones, and the interiors section of data-and-images.md now points at the suite that enforces its rules. Docs only; no source changes. Verified by running the suite (15 suites, 41 tests, all passing) and by confirming from PowerShell — which does not expand globs — that the runner does its own pattern matching. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
25 KiB
Art Gallery — setup and operations
Prerequisites
- Node.js 20+ (verified on 24.19;
npm testrelies onnode --testexpanding**itself, which older majors may not do) - PostgreSQL reachable from the app host
- Network access to Wikipedia / Wikimedia (for biographies, seeding, and on-demand image fetch)
Environment
Development-first: The root .env is for dev only (DB_NAME=gallery_dev). All routine work — code, migrations, seeds, image scripts, curator edits — targets dev. Production uses infra/docker/.env.prod and TrueNAS; promote on a scheduled release (~weekly), not after every change. See environments.md — Development-first workflow.
Copy the example file and fill in credentials:
cp .env.example .env
| Variable | Description |
|---|---|
DB_HOST |
PostgreSQL host |
DB_PORT |
Port (default 5432) |
DB_USER |
Database user |
DB_PASSWORD |
Database password |
DB_NAME |
Database name (gallery_dev for dev; prod uses gallery_prod) |
PORT |
API listen port (3451 for dev:web; prod container uses 5173) |
HOST |
Bind address (default 0.0.0.0 — required for LAN access) |
PUBLIC_URL |
Public URL (dev: https://devgallery.mysuperlab.netcraze.pro; prod: https://gallery.mysuperlab.netcraze.pro) |
TRUST_PROXY |
Set to true when behind nginx/reverse proxy (honours X-Forwarded-*) |
IMAGE_DIR |
Root for cached images (default ./data/images) |
SESSION_SECRET |
Random string for signed session cookies (required for curator login) |
SESSION_COOKIE_SECURE |
Optional — omit for auto (HTTPS via proxy → Secure); set true/false to force |
CURATOR_USERNAME |
Bootstrap / reset — curator account name (default curator) |
CURATOR_PASSWORD |
Bootstrap when users is empty; also used by npm run dev:reset-curator |
.env is git-ignored; never commit passwords.
Optional script tuning:
| Variable | Used by | Description |
|---|---|---|
MIN_PAINTINGS |
expand-catalog |
Minimum paintings per artist (default 6) |
FETCH_MAX_WAIT_SEC |
fetch-images |
Max seconds per painting in batch runs (default 10) |
Install
npm install
cd client && npm install && cd ..
Database bootstrap
One-shot setup (migrate + seed):
npm run dev:setup
Or step by step:
npm run dev:migrate # db/schema.sql + db/migrate-*.sql via server/migrate.js
npm run dev:seed # eras, movements, artists, flagship paintings (one per artist)
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).
Curator accounts (auth migration)
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. To sync the password from .env later, run npm run dev:reset-curator.
After migrate, sign in from the site header (Curator login). Debug mode, Checkup, Translations, Influences, Movement links (top-left), Tour editor, and all mutating debug APIs require an active curator session (Movement links opens a login gate for guests). Anonymous visitors browse the timeline, published Tours, and 3D halls without logging in. dev:migrate also creates movement_lineage_links / movement_lineage_delete_votes and seeds default lineage when the table is empty.
See API.md — Authentication and basics.md — Developer tools.
Recommended post-seed steps
After a fresh seed, run these to match a fully populated local install:
npm run dev:sync-image-paths # import paintings from data/images/paintings/ (clone with image files)
npm run dev:fetch-artist-images # link local portraits or download from Wikipedia
npm run dev:fetch-artist-bios # bio_short / bio_full from Wikipedia
npm run dev:expand-catalog # famous works for artists below MIN_PAINTINGS
npm run dev:update-influences # painting influence graph for detail view + hall exits
npm run dev:migrate:checkup-flags # optional: review/fixed flags for Checkup page (paintings)
npm run dev:migrate:artist-checkup-flags # optional: same flags for artist portraits (bio debug)
npm run dev:migrate:search # optional on very old DBs — also applied by dev:migrate / prod Step 4
npm run dev:migrate:i18n # optional — entity_translations (also in dev:migrate)
npm run dev:fetch-artist-bios-ru # draft Russian bios + Cyrillic names from ru.wikipedia
npm run dev:import-translations -- --file path/to/file.json # bulk translation import
npm run dev:migrate:painting-annotations # optional: art-history notes table
npm run dev:update-painting-annotations # optional: load curated notes (+ --wikipedia for Wikipedia intros)
npm run dev:fetch-images -- --limit=50 # random sample; 10s max per painting (default)
npm run dev:fetch-images -- --limit=50 --max-wait=120 # same batch size, longer lookup per work
cd client && npm run build && cd ..
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 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
| Command | Purpose |
|---|---|
npm test |
Test suite — 15 suites / 41 tests on Node’s built-in runner; no database or server needed |
npm run dev:web |
Public dev stack — Vite :5173, API :3451 (Keenetic → devgallery…) |
npm run prod:docker:publish |
Build + push prod image to Gitea |
npm run devtoprod:images |
Copy data/images/ → TrueNAS via SMB Gallery share |
npm run prodto:dev:images |
Copy prod images → dev repo |
npm run prodto:dev:db |
Clone gallery_prod → gallery_dev |
npm run dev:db:backup / devtoprod:db:restore |
Dev backup / promote catalog DB to prod (prod restore skips staff/session/audit; syncs sequences) |
npm run harmonize |
Bidirectional catalog + image merge (last-write-wins) — harmonize-dev-prod.md |
npm run prod:build |
Build production SPA into client/dist |
npm run dev:start |
API + static SPA on HOST:PORT (uses root .env) |
npm run prod:start |
Build client, then start server |
npm run dev:server |
API with nodemon reload (local HMR workflow) |
npm run dev:client |
Vite dev server on :5173 |
See environments.md for dev/prod URLs, database split, Docker deploy, and sync commands. For Russian UI + catalog text, see i18n-russian.md. Timeline layouts and Tree of Art: basics.md, movement-tree.md. UI behaviour standards: ui-interaction-and-component-standards.md. Influence link import/CRUD: influence-import.md. Quick reference: FAC.md.
Production frontend: build the client, then start the server:
npm run prod:build
# or: cd client && npm run build && cd ..
npm run dev:start
Open http://localhost:5173 (Vite) or http://localhost:3451 (API only).
Production deployment
Production runs as gallery-web on TrueNAS at https://gallery.mysuperlab.netcraze.pro (Keenetic → :5173). Full guide: environments.md and infra/docker/DEPLOY-truenas.md.
| Access | URL |
|---|---|
| Production (Keenetic) | https://gallery.mysuperlab.netcraze.pro |
| Development (Keenetic) | https://devgallery.mysuperlab.netcraze.pro |
| LAN direct (prod container) | http://192.168.10.122:5173 |
| LAN direct (dev PC) | http://192.168.10.70:5173 |
Quick deploy checklist:
- One-time DB split in pgAdmin on dev PC:
db/split-dev-prod-pgadmin.sql - Dev
.env→DB_NAME=gallery_dev,PORT=3451,PUBLIC_URL=https://devgallery.mysuperlab.netcraze.pro, plus auth vars (SESSION_SECRET,CURATOR_*) npm run dev:migrateon dev and prod DBs (includes auth tables + bootstrap curator)net use \\192.168.10.122\Gallery→npm run devtoprod:imagesnpm run prod:docker:publish→ TrueNAS Custom App frominfra/docker/compose.truenas.yaml(set auth env in compose)- Keenetic: both domains →
:5173, protocol to devicehttp, correct IP per environment
Legacy deployment (optional)
Node on the dev PC at :3520 with nginx → Vite :5173 is superseded by TrueNAS Docker prod and npm run dev:web for public dev. See deploy/nginx-gallery.conf and deploy/gallery.service only if you need a local nginx/systemd setup.
Maintenance scripts
| Command | Script | Purpose |
|---|---|---|
npm run dev:seed |
scripts/seed-wikipedia.js |
Reload curated Wikipedia data |
npm run dev:fetch-artist-bios |
scripts/fetch-artist-bios.js |
Wikipedia lead sections → bio_short / bio_full |
npm run dev:fetch-artist-bios -- --force |
↑ | Refresh bios even when already set |
npm run dev:expand-catalog |
scripts/expand-paintings.js |
Insert famous works from famous-paintings-data.js |
npm run dev:expand-catalog -- --fetch-images |
↑ | Also download images for new rows (slow) |
npm run dev:fetch-images |
scripts/fetch-missing-images.js |
Search Wikipedia, Commons, museums for missing files |
npm run dev:search-missing-paintings |
↑ (alias) | Same as fetch-images |
npm run dev:fetch-images -- --artist="Name" |
↑ | Limit to one artist (catalog order) |
npm run dev:fetch-images -- --limit=50 |
↑ | Random sample of N missing paintings |
npm run dev:fetch-images -- --limit=250 |
↑ | Larger random batch (processes min(N, missing count)) |
npm run dev:fetch-images -- --limit=50 --max-wait=120 |
↑ | Random batch; 120s cap per painting |
npm run dev:fetch-images -- --discover-only --limit=20 |
↑ | Fix wikipedia_title via search only |
npm run dev:regenerate-thumbnails |
scripts/regenerate-thumbnails.js |
Rebuild all thumbs from full local files |
npm run dev:audit-painting-images |
scripts/audit-painting-images.js |
List thumb/full aspect-ratio mismatches |
npm run dev:migrate:thumbnails |
db/migrate-thumbnails.sql |
Add thumbnail columns |
npm run dev:fetch-artist-images |
scripts/fetch-artist-images.js |
Download portraits or link existing files under data/images/portraits/ |
npm run dev:fetch-artist-images -- --force |
↑ | Re-fetch even when portrait_path is set |
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 dev:sync-image-paths -- --dry-run |
↑ | Report only, no DB writes |
npm run dev:migrate:influence-sources |
scripts/migrate-influence-sources.js |
Create painting_influence_sources + backfill legacy edges |
npm run dev:migrate:checkup-flags |
scripts/migrate-checkup-flags.js |
Add checkup_checked / checkup_fixed on paintings |
npm run dev:migrate:artist-checkup-flags |
scripts/migrate-artist-checkup-flags.js |
Add checkup_checked / checkup_fixed on artists (bio debug) |
npm run dev:migrate:search |
scripts/migrate-search.js |
Same indexes as migrate-search.sql (also run by dev:migrate) |
npm run dev:migrate:painting-annotations |
scripts/migrate-painting-annotations.js |
Create painting_annotations table |
npm run dev:migrate:artist-palette |
scripts/migrate-artist-palette.js |
Add palette_metadata JSONB on artists |
npm run dev:import-painter-palette |
scripts/import-painter-palette.js |
Enrich artists + influence links from Inputs/PainterPalette.csv |
npm run dev:analyze-painter-palette |
scripts/analyze-painter-palette.js |
Report CSV ↔ gallery artist name matches |
npm run dev:export-paintings |
scripts/export-paintings-csv.js |
Write Output/paintings.csv (artist, painting, year) |
npm run dev:update-painting-annotations |
scripts/update-painting-annotations.js |
Load curated notes from painting-annotations-data.js |
npm run dev:update-painting-annotations -- --wikipedia |
↑ | Add intro sentences from each work’s Wikipedia page |
npm run dev:update-painting-annotations -- --wikipedia --wiki-delay=3000 |
↑ | Slower Wikipedia pass when rate-limited (429) |
npm run dev:find-duplicates |
scripts/find-duplicate-paintings.js |
Report duplicate and near-duplicate painting rows |
npm run dev:audit-influence-duplicates |
scripts/audit-influence-duplicates.js |
Report mirrored legacy/sources edges and duplicate influence rows |
npm run dev:update-influences |
scripts/update-influences.js |
Insert influence links (painting / artist / movement) from art-influences-data.js |
npm run dev:update-influences -- --fetch-images |
↑ | Also download images for newly created works |
npm run dev:update-influences -- --discover |
↑ | Curated pass + web discovery (Wikipedia, Wikidata, Met, art-history sites) |
npm run dev:discover-influences |
update-influences.js --discover-only |
Discovery pass only |
Scripts in the repository
These are checked in and maintained:
seed-wikipedia.js,seed-catalog-data.js— initial catalog (npm run dev:seed)image-fetcher.js— Wikimedia / museum image resolutionfetch-artist-images.js— artist portrait download / disk linkingsync-image-paths.js— import paintings and align paths fromdata/images/paintings/fetch-artist-bios.js— artist biographiesexpand-paintings.js+famous-paintings-data.js— catalog expansionupdate-influences.js+art-influences-data.js— influence graph (paintings, artists, movements)influence-discovery.js+influence-resolver.js— web discovery and polymorphic source resolutionfetch-missing-images.js— batch image backfillfind-duplicate-paintings.js— duplicate catalog auditaudit-influence-duplicates.js— influence graph duplicate / mirror auditpainter-palette-lib.js,import-painter-palette.js,migrate-artist-palette.js,analyze-painter-palette.js— PainterPalette CSV integrationexport-paintings-csv.js— catalog CSV export toOutput/migrate-checkup-flags.js— checkup workflow columns (paintings)migrate-artist-checkup-flags.js— checkup workflow columns (artist portraits)migrate-painting-annotations.js,update-painting-annotations.js,painting-annotations-data.js— art-history notes on painting detailregenerate-thumbnails.js,audit-painting-images.js
See data-and-images.md for pipeline details and override maps.
Remote repository
Gitea: Danilka/Art-gallery
git clone https://gitea.mysuperlab.netcraze.pro/Danilka/Art-gallery.git
After clone: copy .env.example → .env, install dependencies, run one-time DB split, then post-seed steps. Deploy: environments.md.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| 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 |
| 503 on public URL | Dev servers not running | npm run dev:web (or dev:server + dev:client for local HMR) |
/api proxy errors on localhost while Vite works |
Shell PORT=5173 makes API bind the same port as Vite |
Set PORT=3451 in .env or DEV_API_PORT=3451; restart npm run dev:web |
| Timeline opens classic after bookmark | Wrong or missing ?layout= |
Use ?layout=tree or ?layout=vertical; classic omits the param or uses ?layout=classic — see basics.md |
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 |
| “Biographical information not yet available” | Bios not fetched | npm run dev:fetch-artist-bios |
| Placeholder portraits on timeline | portrait_path not set |
npm run dev:fetch-artist-images |
| Artist hall has only 1–2 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 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 |
| 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 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 |
| 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 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 |
| 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 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 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 dev:update-influences |
| 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 |
| 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:web or npm run dev:server after pulling API changes |
| 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 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 — nothing happens after choosing file | Stale client or broken hidden-file click() |
Pull latest client (DebugUploadButton uses <label> + <input>); hard-refresh |
| Uploaded image reverts after reload (old picture) | Browser cached /images/… at same path |
Pull latest server + client — API returns image_cache_key and URLs use ?v=; /images no longer uses immutable long-term cache |
| 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 |
| Fixed image not shown in 3D gallery | Stale gallery session or cached texture | Rebuild client; fix/upload updates session + ?v= from image_cache_key — use Back to Gallery (not browser back) |
| Back to Timeline returns to gallery / previous wing | Stale client build | Pull latest client — goToTimelineHome() unmounts the hall and resets timeline zoom |
| Catalog search dropdown hidden under timeline | Stale client CSS | Rebuild client — .site-header uses z-index: 110 above the sticky timeline bar |
| Catalog search returns empty / 500 | Search indexes missing | Run npm run dev:migrate (includes migrate-search.sql) or npm run dev:migrate:search; restart API |
fetch-artist-bios-ru fails: index row size … exceeds btree maximum |
Old entity_translations index on all value text |
npm run dev:migrate (partial search index on name/title only), then re-run npm run dev:fetch-artist-bios-ru |
| Russian UI shows English catalog names | Translations not published | Curator → Translations → Publish; public API serves only status = published |
| Influence import leaves many unresolved tokens | Names not in catalog DB | Curator → Influences → Import warnings; fix spelling or add artists first; free-text traditions stay unresolved |
| Influence import created too many edges | Artist-level rows expand to all paintings | Expected (PainterPalette-style); delete unwanted edges in Influences list |
| Influence import blocked: already imported | Same file bytes or mapped data imported before | Expected; use Import anyway only if intentional, or skip |
| 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 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: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 |
| 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 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 dev:migrate (or npm run dev:reset-curator), 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 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 |
| Dark center entering 3D hall (header visible, no spinner) | Stale client before gallery loading overlay fix | Hard-refresh; latest client shows Loading gallery… / Loading paintings… in the canvas area until ready |
| 3D hall loading overlay never clears | Stuck texture counter or hung HDR | Hard-refresh; current client times out Environment / shader warm-up so the overlay cannot stay forever |
| 3D hall black after returning from painting detail | WebGL context lost while hall was hidden | Hard-refresh; latest client remounts canvas when hall becomes active again |