From 44d3d4359a5473eb6724eb48aa99be9e1ae2b400 Mon Sep 17 00:00:00 2001 From: Danila Khodjaef Date: Fri, 19 Jun 2026 09:46:44 +0300 Subject: [PATCH] Add project documentation under Documentation/. Covers architecture, setup, database schema, API reference, and image pipeline; link from README. Co-authored-by: Cursor --- Documentation/API.md | 153 +++++++++++++++++++++++++++++++ Documentation/DB_structure.md | 147 +++++++++++++++++++++++++++++ Documentation/basics.md | 88 ++++++++++++++++++ Documentation/data-and-images.md | 76 +++++++++++++++ Documentation/setup.md | 103 +++++++++++++++++++++ README.md | 10 ++ 6 files changed, 577 insertions(+) create mode 100644 Documentation/API.md create mode 100644 Documentation/DB_structure.md create mode 100644 Documentation/basics.md create mode 100644 Documentation/data-and-images.md create mode 100644 Documentation/setup.md diff --git a/Documentation/API.md b/Documentation/API.md new file mode 100644 index 0000000..56d48cd --- /dev/null +++ b/Documentation/API.md @@ -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/` 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/` or placeholder | +| `galleryImageUrl(painting)` | Local thumb/full only (3D) | +| `paintingImageUrl(painting)` | Local file or on-demand API | diff --git a/Documentation/DB_structure.md b/Documentation/DB_structure.md new file mode 100644 index 0000000..39c4bc2 --- /dev/null +++ b/Documentation/DB_structure.md @@ -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 artist’s 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. diff --git a/Documentation/basics.md b/Documentation/basics.md new file mode 100644 index 0000000..f822448 --- /dev/null +++ b/Documentation/basics.md @@ -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 artist’s 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 | diff --git a/Documentation/data-and-images.md b/Documentation/data-and-images.md new file mode 100644 index 0000000..daec44c --- /dev/null +++ b/Documentation/data-and-images.md @@ -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 artist’s 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. diff --git a/Documentation/setup.md b/Documentation/setup.md new file mode 100644 index 0000000..a71758c --- /dev/null +++ b/Documentation/setup.md @@ -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 | diff --git a/README.md b/README.md index b91705e..4ffaab6 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,16 @@ 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 - **Backend:** Node.js, Express, PostgreSQL