Add project documentation under Documentation/.

Covers architecture, setup, database schema, API reference, and image pipeline; link from README.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-06-19 09:46:44 +03:00
co-authored by Cursor
parent 40db99d734
commit 44d3d4359a
6 changed files with 577 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
# Art Gallery — REST API
Base URL in development:
- **Direct:** `http://localhost:3001`
- **Via Vite proxy:** `http://localhost:5173` (same paths)
All JSON responses use `Content-Type: application/json`. Errors return `{ "error": "message" }` with an appropriate HTTP status.
Static images are served at `/images/<relative-path>` from `IMAGE_DIR`.
---
## `GET /api/bounds`
Returns the overall timeline year range used to initialise the zoomable timeline.
**Response**
```json
{
"min_year": -800,
"max_year": 2100
}
```
`min_year` is the earliest art movement start; `max_year` is the latest of era ends, movement ends, and artist death years.
---
## `GET /api/timeline`
Historical eras and art movements overlapping a year window.
**Query**
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `start` | int | -3000 | Window start year |
| `end` | int | 2100 | Window end year |
**Response**
```json
{
"eras": [ { "id": 1, "name": "Renaissance", "start_year": 1300, "end_year": 1600, "start_definite": false, "end_definite": false, "description": "...", "sort_order": 3 } ],
"movements": [ { "id": 5, "name": "Impressionism", "start_year": 1860, "end_year": 1890, "era_id": 6, "era_name": "Modern", "color": "#87CEEB", ... } ]
}
```
Movements are filtered to those with at least one artist active in the requested range.
---
## `GET /api/artists`
Artists for timeline portraits and movement bands.
**Query**
| Param | Type | Description |
|-------|------|-------------|
| `start` | int | Only artists alive after this year |
| `end` | int | Only artists born before this year |
| `movement_id` | int | Filter by movement |
**Response** — array of artist objects with joined `movement_name` and `movement_color`.
---
## `GET /api/movements/:id/artists`
Artists belonging to a single movement.
**Response** — array of `{ id, name, birth_year, death_year, portrait_path, bio_short }`.
---
## `GET /api/artists/:id`
Full artist profile for the bio page and 3D gallery entry.
**Response**
```json
{
"artist": { "id": 1, "name": "...", "movement_name": "...", "bio_full": "...", ... },
"periods": [ { "id": 1, "name": "Milan Period", "start_year": 1482, "end_year": 1499, ... } ],
"paintings": [ { "id": 10, "title": "...", "year": 1498, "image_path": "...", "thumbnail_path": "...", ... } ]
}
```
---
## `POST /api/artists/:id/preload-images`
Fast local scan: links paintings to files already on disk. Does **not** download from the internet (safe to call before opening the 3D gallery).
**Response**
```json
{ "fetched": 12, "total": 15 }
```
`fetched` counts paintings with a resolvable local image after the scan.
---
## `GET /api/paintings/:id`
Painting detail with influence graph neighbours.
**Response**
```json
{
"painting": { "id": 10, "title": "...", "artist_name": "...", "image_path": "...", ... },
"influencedBy": [ { "id": 3, "title": "...", "artist_name": "...", "notes": "...", "aspects": "...", "quote": "...", "source_author": "...", ... } ],
"influenced": [ { "id": 20, "title": "...", ... } ]
}
```
---
## `GET /api/paintings/:id/image`
Serves a painting image file. Resolves and caches from external sources only when no local file exists.
**Query**
| Param | Values | Default |
|-------|--------|---------|
| `size` | `thumb`, `full` | `thumb` |
Returns the image bytes with `Cache-Control: public, max-age=86400`, or `404` if unavailable.
---
## Frontend helpers
The React client wraps these endpoints in `client/src/api/client.ts`:
| Function | Maps to |
|----------|---------|
| `api.getBounds()` | `GET /api/bounds` |
| `api.getTimeline(start, end)` | `GET /api/timeline` |
| `api.getArtists(...)` | `GET /api/artists` |
| `api.getArtist(id)` | `GET /api/artists/:id` |
| `api.getPainting(id)` | `GET /api/paintings/:id` |
| `preloadArtistImages(id)` | `POST /api/artists/:id/preload-images` |
| `imageUrl(path)` | `/images/<path>` or placeholder |
| `galleryImageUrl(painting)` | Local thumb/full only (3D) |
| `paintingImageUrl(painting)` | Local file or on-demand API |
+147
View File
@@ -0,0 +1,147 @@
# Art Gallery — database structure
PostgreSQL schema for the virtual gallery. Canonical DDL is intended to live in `db/schema.sql` when checked in; this document describes the logical model either way.
Connection settings come from `.env` (see [setup.md](setup.md)).
## Overview
| Item | Typical value |
|------|----------------|
| Engine | PostgreSQL 14+ |
| Database | `Gallery` |
| App user | `gallery` |
| Time fields | Integer years (negative = BCE) |
## Entity relationship
```mermaid
erDiagram
historical_eras ||--o{ art_movements : contains
art_movements ||--o{ artists : groups
artists ||--o{ artist_periods : has
artists ||--o{ paintings : created
artist_periods ||--o{ paintings : groups
paintings ||--o{ painting_influences : influenced_by
paintings ||--o{ painting_influences : influences
```
## Tables
### `historical_eras`
Broad chronological buckets (Ancient, Medieval, Renaissance, …).
| Column | Type | Notes |
|--------|------|-------|
| `id` | SERIAL PK | |
| `name` | VARCHAR(100) | Display label |
| `start_year`, `end_year` | INTEGER | Inclusive range |
| `start_definite`, `end_definite` | BOOLEAN | `false` → render as gradient edge on timeline |
| `description` | TEXT | Tooltip / sidebar copy |
| `sort_order` | INTEGER | Display order |
### `art_movements`
Finer-grained styles (Impressionism, Cubism, Suprematism, …).
| Column | Type | Notes |
|--------|------|-------|
| `id` | SERIAL PK | |
| `name` | VARCHAR(150) | |
| `start_year`, `end_year` | INTEGER | |
| `start_definite`, `end_definite` | BOOLEAN | Same visual semantics as eras |
| `era_id` | FK → `historical_eras` | Optional parent era |
| `description` | TEXT | |
| `color` | VARCHAR(20) | Hex colour for movement band |
### `artists`
| Column | Type | Notes |
|--------|------|-------|
| `id` | SERIAL PK | |
| `name` | VARCHAR(200) | |
| `birth_year`, `death_year` | INTEGER | Nullable; used for timeline portrait placement |
| `movement_id` | FK → `art_movements` | Primary movement |
| `portrait_path` | VARCHAR(500) | Relative to `data/images/` |
| `bio_short`, `bio_full` | TEXT | From Wikipedia extracts |
| `wikipedia_title` | VARCHAR(300) | Source page title |
| `century` | INTEGER | Rounded century bucket for seeding limits |
### `artist_periods`
Phases within an artists career (e.g. “Blue Period”, “Roman Period”).
| Column | Type | Notes |
|--------|------|-------|
| `id` | SERIAL PK | |
| `artist_id` | FK → `artists` | ON DELETE CASCADE |
| `name` | VARCHAR(200) | |
| `start_year`, `end_year` | INTEGER | |
| `description` | TEXT | |
| `sort_order` | INTEGER | Wall order in 3D gallery |
### `paintings`
| Column | Type | Notes |
|--------|------|-------|
| `id` | SERIAL PK | |
| `artist_id` | FK → `artists` | ON DELETE CASCADE |
| `period_id` | FK → `artist_periods` | Optional grouping |
| `title` | VARCHAR(300) | |
| `year`, `year_end` | INTEGER | Creation date(s) |
| `description` | TEXT | |
| `image_path` | VARCHAR(500) | Full-size local file |
| `thumbnail_path` | VARCHAR(500) | Smaller variant for lists / 3D |
| `wikipedia_title` | VARCHAR(300) | Used by image fetcher |
| `sort_order` | INTEGER | |
### `painting_influences`
Directed edges: *this painting* was influenced by *that painting*.
| Column | Type | Notes |
|--------|------|-------|
| `id` | SERIAL PK | |
| `painting_id` | FK → `paintings` | The work being explained |
| `influenced_by_painting_id` | FK → `paintings` | The earlier / source work |
| `notes` | TEXT | Curator summary |
| `source` | VARCHAR(500) | General attribution |
| `aspects` | TEXT | What was borrowed (composition, colour, …) |
| `quote` | TEXT | Short citation |
| `source_author` | VARCHAR(200) | e.g. Gombrich, Janson |
| `source_url` | VARCHAR(500) | Reference link |
Unique constraint on `(painting_id, influenced_by_painting_id)`.
## Indexes
- `artists(movement_id)`, `artists(century)`
- `paintings(artist_id)`, `paintings(period_id)`
- `art_movements(era_id)`, `art_movements(start_year, end_year)`
- `painting_influences(painting_id)`, `painting_influences(influenced_by_painting_id)`
## First-time setup
The `gallery` database user needs `CREATE` on schema `public` for migrations. If tables cannot be created, run admin grants as PostgreSQL superuser before `npm run migrate`:
```sql
GRANT CREATE ON SCHEMA public TO gallery;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO gallery;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO gallery;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO gallery;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO gallery;
```
Then:
```bash
npm run migrate
npm run seed
```
## Data conventions
- **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`.
- **Seeding cap**: curated ingest targets at most ~100 artists per century to keep the catalog manageable.
+88
View File
@@ -0,0 +1,88 @@
# Art Gallery — architecture basics
Interactive virtual museum spanning art history: zoomable timeline, movement bands, 3D gallery halls, and painting influence graphs. See [README.md](../README.md) for quick start.
## Concept
The app is organised as a **drill-down hierarchy**:
1. **Timeline** — historical eras (Ancient → Contemporary) with definite or fuzzy date boundaries.
2. **Movement bands** — art movements aligned to the same time axis, each showing portrait thumbnails of key artists.
3. **3D gallery** — classic hall environment; paintings grouped by the artists creative periods.
4. **Painting detail** — full work in the centre, *Influenced By* on the left, *Influenced* on the right, link to artist biography.
All artwork images are stored locally under `data/images/` — the UI never hot-links to Wikipedia or Commons at runtime (except optional on-demand fetch when a file is missing).
## Stack
| Layer | Choice | Role |
|--------|--------|------|
| API | **Node.js + Express 5** | REST API, static image serving, optional SPA hosting from `client/dist` |
| Database | **PostgreSQL** | Eras, movements, artists, paintings, influence edges |
| Frontend | **React 19 + Vite 8** | SPA routing and UI |
| 3D | **Three.js** via `@react-three/fiber`, `@react-three/drei` | Virtual gallery navigation |
| Data ingestion | Node scripts | Wikipedia summaries, Commons images, curated influence data |
## Repository layout
```text
Gallery/
├── server/ # Express API, DB pool, image service
├── client/ # React/Vite frontend
│ ├── src/ # Source (components, pages, 3D scene)
│ └── dist/ # Production build (served by API when present)
├── scripts/ # Seed, image fetch, catalog expansion
├── data/images/ # Local portraits and paintings (+ thumbs/)
├── db/ # SQL schema and migrations (when present)
├── Documentation/ # This folder
└── .env # DB and port config (not committed)
```
## Runtime modes
### Production-style (single process)
```bash
npm run server # http://localhost:3001
```
Serves `/api/*`, `/images/*`, and the built SPA from `client/dist` if it exists.
### Development (two processes)
```bash
npm run dev:server # API on :3001
npm run dev:client # Vite on :5173, proxies /api and /images
```
Use the Vite URL during frontend work for HMR.
## User navigation flow
```mermaid
flowchart TD
A[Home — timeline + movements] -->|scroll / drag / zoom| A
A -->|click portrait| B[Artist bio]
B -->|Enter Gallery| C[3D hall]
C -->|click painting| D[Painting detail + influences]
D -->|thumbnail left/right| D
D -->|artist link| B
B -->|Back| A
C -->|Back| A
```
## Key design decisions
- **Timeline bounds** derive from the earliest art movement start year, not ancient-era metadata alone, so the default view opens where catalogued content begins.
- **Movement filtering** on zoom only shows movements that have at least one artist active in the visible year range.
- **3D gallery images** use locally cached files only (`galleryImageUrl`); slow remote fetches would break realtime rendering.
- **Influence data** is stored as directed edges between paintings, with optional citation fields (source author, quote, URL) for art-historical references.
## Related docs
| Document | Contents |
|----------|----------|
| [setup.md](setup.md) | Install, database, npm scripts |
| [DB_structure.md](DB_structure.md) | Tables and relationships |
| [API.md](API.md) | REST endpoints |
| [data-and-images.md](data-and-images.md) | Image pipeline and seeding |
+76
View File
@@ -0,0 +1,76 @@
# Art Gallery — data and images
How catalog content and artwork files enter the system.
## Principles
1. **No runtime hot-linking** — the UI reads from `/images/…` (local disk). External URLs are used only during ingest.
2. **No AI-generated art or text** — biographies and descriptions come from Wikipedia; influence notes from curated art-history sources.
3. **Local copies** — every displayed image should exist under `data/images/` after seeding or fetch.
## Directory layout
```text
data/images/
├── portraits/ # Artist headshots
│ └── Claude_Monet.jpg
└── paintings/
├── Claude_Monet_Water_Lilies.jpg
└── thumbs/
└── Claude_Monet_Water_Lilies_thumb.jpg
```
File names are sanitised `{Artist}_{Title}.{ext}`. The image service can rediscover files on disk even when DB paths are empty (`server/image-service.js``syncPaintingFromDisk`).
## Seeding pipeline
`npm run seed` runs `scripts/seed-wikipedia.js`, which:
1. Inserts **historical eras** and **art movements** (curated date ranges and colours).
2. For each curated **artist**:
- Fetches Wikipedia intro text for `bio_short` / `bio_full`.
- Downloads a Commons portrait into `portraits/`.
- Creates **artist periods** and **paintings**.
- Downloads painting images into `paintings/`.
3. Writes **painting_influences** edges from curated scholarship references.
Artists are grouped by movement and century; the seed list targets at most ~100 artists per century.
## On-demand image resolution
When a painting has no local file, `GET /api/paintings/:id/image` triggers `ensurePaintingImages()`:
1. Check DB paths → verify file on disk.
2. Scan disk by `{artist}_{title}` pattern.
3. If still missing and `wikipedia_title` is set, call `scripts/image-fetcher.js`:
- Wikidata → Wikimedia Commons → Wikipedia page image
- Fallbacks: Met Museum, Art Institute of Chicago, Rijksmuseum APIs
4. Save full + thumbnail, update DB, serve file.
Requests are deduplicated (`inflight` map) and timeout after 15 seconds. A 2.5 s delay between external requests reduces rate-limit risk.
## Preload before 3D gallery
`POST /api/artists/:id/preload-images` runs **local-only** linking — no network. Call this when entering an artists 3D hall so textures use files already on disk.
The 3D scene uses `galleryImageUrl()`, which never hits the on-demand API (remote latency breaks WebGL texture loading).
## Placeholders
When no image is available:
- `/placeholder-portrait.svg` — timeline / movement band portraits
- `/placeholder-art.svg` — paintings in lists and detail view
These live in `client/public/` (and `client/dist/` after build).
## Adding new artists manually
1. Insert rows into `artists`, `artist_periods`, `paintings` (or extend the `ARTISTS` array in the seed script).
2. Place image files under `data/images/` using the naming convention.
3. Run `npm run sync-image-paths` if that script is available, or rely on preload / on-demand sync.
4. Add influence rows to `painting_influences` with citation fields where possible.
## Image fetcher overrides
`scripts/image-fetcher.js` includes hand-maintained overrides for ambiguous Wikipedia titles and direct URLs (e.g. works whose Commons name does not match the article title). Extend `PAINTING_WIKI_OVERRIDES` and `DIRECT_IMAGE_OVERRIDES` when automated resolution fails.
+103
View File
@@ -0,0 +1,103 @@
# Art Gallery — setup and operations
## Prerequisites
- **Node.js** 20+
- **PostgreSQL** reachable from the app host
- Network access to Wikipedia / Wikimedia (only for seeding and on-demand image fetch)
## Environment
Copy the example file and fill in credentials:
```bash
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`) |
| `PORT` | API listen port (default `3001`) |
| `IMAGE_DIR` | Root for cached images (default `./data/images`) |
`.env` is git-ignored; never commit passwords.
## Install
```bash
npm install
cd client && npm install && cd ..
```
## Database bootstrap
One-shot setup (migrate + seed):
```bash
npm run setup
```
Or step by step:
```bash
npm run migrate # apply db/schema.sql
npm run seed # eras, movements, artists, paintings, influences
```
If migration fails with permission errors, grant schema rights to the app user first (see [DB_structure.md](DB_structure.md)).
## Run
| Command | Purpose |
|---------|---------|
| `npm run server` | API + static SPA on `PORT` |
| `npm run dev:server` | API with nodemon reload |
| `npm run dev:client` | Vite dev server on :5173 |
| `npm run dev` | Alias for `server` |
**Production frontend:** build the client, then start the server:
```bash
cd client && npm run build && cd ..
npm run server
```
Open http://localhost:3001 (or your configured `PORT`).
## Maintenance scripts
| Script | Command | Purpose |
|--------|---------|---------|
| Seed | `npm run seed` | Reload curated Wikipedia data |
| Thumbnails migration | `npm run migrate:thumbnails` | Add thumbnail columns |
| Fetch missing images | `npm run fetch-images` | Backfill painting files |
| Fetch artist portraits | `npm run fetch-artist-images` | Backfill portrait files |
| Sync image paths | `npm run sync-image-paths` | Align DB paths with disk |
| Expand catalog | `npm run expand-catalog` | Add paintings from Wikipedia lists |
| Update influences | `npm run update-influences` | Refresh influence edges |
Scripts under `scripts/` that are not yet present locally may need to be restored from git history or re-added; core paths (`seed-wikipedia.js`, `image-fetcher.js`) are in the repository.
## Remote repository
Gitea: [Danilka/Art-gallery](https://gitea.mysuperlab.netcraze.pro/Danilka/Art-gallery)
```bash
git clone https://gitea.mysuperlab.netcraze.pro/Danilka/Art-gallery.git
```
After clone: copy `.env.example``.env`, install dependencies, run `npm run setup` against your Postgres instance.
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| Empty timeline | DB not seeded | `npm run seed` |
| 500 on all `/api/*` | Wrong `.env` or Postgres down | Check connection, logs |
| Black frames in 3D gallery | No local image for painting | `POST …/preload-images` or `npm run fetch-images` |
| 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 |
+10
View File
@@ -2,6 +2,16 @@
Interactive virtual art gallery: zoomable historical timeline, art movement bands, 3D navigable gallery halls, and painting influence graphs. Interactive virtual art gallery: zoomable historical timeline, art movement bands, 3D navigable gallery halls, and painting influence graphs.
## Documentation
| Document | Purpose |
|----------|---------|
| [Documentation/basics.md](Documentation/basics.md) | Architecture, layout, user flow |
| [Documentation/setup.md](Documentation/setup.md) | Install, env, npm scripts |
| [Documentation/DB_structure.md](Documentation/DB_structure.md) | PostgreSQL tables |
| [Documentation/API.md](Documentation/API.md) | REST endpoints |
| [Documentation/data-and-images.md](Documentation/data-and-images.md) | Seeding and image pipeline |
## Stack ## Stack
- **Backend:** Node.js, Express, PostgreSQL - **Backend:** Node.js, Express, PostgreSQL