Add curator Movement links config with proof URLs and admin-gated delete votes.
Persist lineage in PostgreSQL, seed documented edges, and keep timeline/tree consumers API-backed with a static fallback. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
2b4cdc5a9f
commit
1b760faf0d
36 files changed
+2051
-43
No files matched your search
+21
-1
@@ -125,11 +125,14 @@ Each list entry includes `created_at`, `username`, `user_role`, `action`, `resou
|
||||
| `POST /api/artists/:id/upload-portrait` | `images` | `artist.upload_portrait` |
|
||||
| `/api/translations/*` | `translations` | `translation.*` |
|
||||
| `/api/influences/*` | `influences` | `influence.*` |
|
||||
| `POST` `/api/movement-links` | `influences` | `movement_link.create` |
|
||||
| `POST`/`DELETE` `/api/movement-links` votes | `influences` | `movement_link.delete_vote*` |
|
||||
| `DELETE` `/api/movement-links/:id` | admin role | `movement_link.delete` |
|
||||
| Tour admin (`/api/tours/admin`, POST/PATCH/DELETE, stops) | `tours` | `tour.*` |
|
||||
| `/api/users/*` | `users` | `user.*` |
|
||||
| `/api/audit/*` | admin role | — (read) |
|
||||
|
||||
**Public** (no login): all catalog `GET` routes, `POST /api/artists/:id/preload-images`, `POST /api/movements/:id/preload-images` (local file linking for 3D halls), `/images`, SPA static.
|
||||
**Public** (no login): all catalog `GET` routes, `GET /api/movement-links`, `POST /api/artists/:id/preload-images`, `POST /api/movements/:id/preload-images` (local file linking for 3D halls), `/images`, SPA static.
|
||||
|
||||
Staff mutations are recorded in `curator_audit_log` with `user_id` (see [DB_structure.md](DB_structure.md)). Browse via admin **Activity** UI or `/api/audit`.
|
||||
|
||||
@@ -261,6 +264,23 @@ Public painting detail still exposes read-only `influencedBy` / `influenced` (un
|
||||
|
||||
---
|
||||
|
||||
## Movement lineage links
|
||||
|
||||
Timeline / Tree of Art predecessor→successor edges. Base path: `/api/movement-links`. See [data-and-images.md](data-and-images.md#movement-lineage-timeline--tree-of-art).
|
||||
|
||||
| Method | Path | Auth | Purpose |
|
||||
|--------|------|------|---------|
|
||||
| `GET` | `/api/movement-links` | public | List links (`q`, pagination); seeds defaults when empty; includes `delete_vote_count` |
|
||||
| `POST` | `/api/movement-links` | curator (`influences`) | Create `{ sourceMovementId, targetMovementId, proofUrl, proofLabel?, linkYearApprox?, notes? }` — **proofUrl required** |
|
||||
| `GET` | `/api/movement-links/:id/delete-votes` | curator (`influences`) | List delete votes (username + reason) |
|
||||
| `POST` | `/api/movement-links/:id/delete-votes` | curator (`influences`) | Vote to delete `{ reason }` (min 10 characters) |
|
||||
| `DELETE` | `/api/movement-links/:id/delete-votes/me` | curator (`influences`) | Withdraw own delete vote |
|
||||
| `DELETE` | `/api/movement-links/:id` | **admin** | Hard delete (curators vote instead) |
|
||||
|
||||
Audit: `movement_link.create` / `movement_link.delete_vote` / `movement_link.delete_vote_withdraw` / `movement_link.delete`.
|
||||
|
||||
---
|
||||
|
||||
## Tours
|
||||
|
||||
Base path: `/api/tours`. Full guide: [tours.md](tours.md).
|
||||
|
||||
@@ -33,6 +33,8 @@ erDiagram
|
||||
paintings ||--o{ painting_influence_sources : influenced_by
|
||||
artists ||--o{ painting_influence_sources : artist_source
|
||||
art_movements ||--o{ painting_influence_sources : movement_source
|
||||
art_movements ||--o{ movement_lineage_links : source
|
||||
art_movements ||--o{ movement_lineage_links : target
|
||||
```
|
||||
|
||||
## Tables
|
||||
@@ -213,6 +215,36 @@ Unique index on `(painting_id, source_type, source_painting_id, source_artist_id
|
||||
|
||||
**Canonical influence store.** Used by all API influence queries: painting detail (`influencedBy`, `influenced`), `has_influence_links`, and artist hall navigation (predecessors / successors). Legacy `painting_influences` rows are backfilled here on migration; new curated painting edges are written to both tables by `update-influences`.
|
||||
|
||||
### `movement_lineage_links`
|
||||
|
||||
Art-movement predecessor → successor edges for the timeline flow and Tree of Art. Migration: `db/migrate-movement-links.sql`.
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | SERIAL PK | |
|
||||
| `source_movement_id` | FK → `art_movements` | Predecessor |
|
||||
| `target_movement_id` | FK → `art_movements` | Successor (must differ from source) |
|
||||
| `link_year_approx` | INTEGER | Approximate transition year |
|
||||
| `proof_url` | TEXT NOT NULL | Required literature / museum / web citation URL |
|
||||
| `proof_label` | TEXT | Short display label for the proof |
|
||||
| `notes` | TEXT | Optional curator note |
|
||||
| `created_by` | FK → `users` | Curator who added the row |
|
||||
| `created_at` / `updated_at` | TIMESTAMPTZ | |
|
||||
|
||||
Unique on `(source_movement_id, target_movement_id)`.
|
||||
|
||||
### `movement_lineage_delete_votes`
|
||||
|
||||
Curator votes requesting deletion of a lineage link. Only **admins** may hard-delete the link. Migration: `db/migrate-movement-link-delete-votes.sql`.
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | SERIAL PK | |
|
||||
| `link_id` | FK → `movement_lineage_links` | Cascade on link delete |
|
||||
| `user_id` | FK → `users` | One vote per user per link |
|
||||
| `reason` | TEXT NOT NULL | Min 10 characters (trimmed) |
|
||||
| `created_at` | TIMESTAMPTZ | |
|
||||
|
||||
### `users`
|
||||
|
||||
Staff accounts (named logins). Anonymous site visitors do not have rows here. Migration: `db/migrate-auth.sql` + `db/migrate-user-roles.sql`.
|
||||
|
||||
@@ -95,7 +95,7 @@ CURATOR_USERNAME=curator
|
||||
CURATOR_PASSWORD=your-secure-password
|
||||
```
|
||||
|
||||
Then open the gallery → **Curator login** (top-right) → use tools allowed by your role/permissions (debug, Checkup, Translations, Influences, Tour editor, **Users**, **Activity**). Mutations are logged in `curator_audit_log` per user.
|
||||
Then open the gallery → **Curator login** (top-right) → use tools allowed by your role/permissions (debug, Checkup, Translations, Influences, **Movement links** top-left, Tour editor, **Users**, **Activity**). Mutations are logged in `curator_audit_log` per user.
|
||||
|
||||
If login fails after changing `CURATOR_PASSWORD` in `.env`, run `npm run dev:reset-curator` (bootstrap only runs when `users` is empty; reset upserts the env account as **admin**).
|
||||
|
||||
@@ -103,9 +103,9 @@ If login fails after changing `CURATOR_PASSWORD` in `.env`, run `npm run dev:res
|
||||
|
||||
| Role | Access |
|
||||
|------|--------|
|
||||
| Guest (`user`) | Timeline, movement flow, 3D halls, painting detail, bios |
|
||||
| Curator | Public browse + assigned permission flags (`images`, `checkup`, `curator_notes`, `translations`, `influences`, `tours`, `users`) |
|
||||
| Admin | All curator tools + **Users** + **Activity** audit reports |
|
||||
| Guest (`user`) | Timeline, movement flow, 3D halls, painting detail, bios; can open **Movement links** login gate |
|
||||
| Curator | Public browse + assigned permission flags (`images`, `checkup`, `curator_notes`, `translations`, `influences`, `tours`, `users`). With `influences`: Influences + Movement links create/vote-to-delete |
|
||||
| Admin | All curator tools + **Users** + **Activity** + hard-delete movement links |
|
||||
|
||||
**Users page:** after admin login, header → **Users** — create/edit staff, reset passwords, disable accounts. Prod and dev keep **separate** `users` tables: `devtoprod:db:restore` and harmonize never copy staff accounts. If create fails with a confusing “already exists” after a restore, serial sequences may be lagging — current restore syncs them to `MAX(id)`, and user create re-syncs `users_id_seq` before insert.
|
||||
|
||||
|
||||
@@ -18,4 +18,5 @@ this file contains draft for future releases and features
|
||||
9. ~~vertical bottom-up timeline (alternative start page)~~ — done: `VerticalTimeline` + `VerticalMovementBands`, header layout switch — [basics.md](basics.md#timeline-and-movement-flow)
|
||||
10. ~~Tree of Art timeline (lineage as a growing tree)~~ — done: `MovementTree` + `movementTree.ts`, shareable `?layout=tree` — [movement-tree.md](movement-tree.md)
|
||||
11. ~~UI interaction and component standards~~ — done: shared doc + Cursor rule for client screens — [ui-interaction-and-component-standards.md](ui-interaction-and-component-standards.md)
|
||||
12. ~~Movement lineage curator config (links + proof URLs + delete votes)~~ — done: `movement_lineage_links` / `movement_lineage_delete_votes`, `MovementLinksPage`, admin-only hard delete — [data-and-images.md](data-and-images.md#movement-lineage-timeline--tree-of-art) / [API.md](API.md#movement-lineage-links)
|
||||
|
||||
@@ -56,6 +56,7 @@ Gallery/
|
||||
│ │ ├── pages/CheckupPage.tsx # Image audit table
|
||||
│ │ ├── pages/TranslationsPage.tsx # Russian translation review
|
||||
│ │ ├── pages/InfluencesPage.tsx # Influence links CRUD + import wizard
|
||||
│ │ ├── pages/MovementLinksPage.tsx # Movement lineage CRUD + delete votes
|
||||
│ │ ├── pages/ToursPage.tsx # Guided tour editor
|
||||
│ │ ├── pages/UsersPage.tsx # Staff accounts
|
||||
│ │ ├── pages/AuditPage.tsx # Admin curator activity reports
|
||||
@@ -63,7 +64,7 @@ Gallery/
|
||||
│ │ ├── i18n/ # react-i18next bootstrap
|
||||
│ │ ├── locales/{en,ru}/ # UI chrome strings (incl. timeline layout + captions)
|
||||
│ │ ├── data/historical-events.ts # Timeline event markers (UI)
|
||||
│ │ ├── data/movement-lineage.ts # Curated movement predecessor links (UI)
|
||||
│ │ ├── data/movement-lineage.ts # Fallback movement predecessor links (UI)
|
||||
│ │ ├── utils/parquetFloorTexture.ts # Procedural parquet floor
|
||||
│ │ ├── utils/debugMode.ts # Debug mode + “Show more” localStorage prefs
|
||||
│ │ ├── utils/timelineView.ts # Shared zoom/pan math for timeline + movements
|
||||
@@ -263,7 +264,7 @@ Each visible movement is drawn as a **portrait-width curved stream** (~54 px str
|
||||
|
||||
| Feature | Implementation |
|
||||
|---------|----------------|
|
||||
| Lineage layout | `client/src/data/movement-lineage.ts` — curated predecessor→successor pairs (Met / ArtStory / museum essays); multiple parents allowed |
|
||||
| Lineage layout | `movement_lineage_links` (DB) via `GET /api/movement-links`; client fallback `movement-lineage.ts`; **Movement links** page (top-left header control; create/vote needs `influences`; hard delete is **admin-only**); multiple parents allowed |
|
||||
| Vertical lanes | Movements share a horizontal lane only when one ends at least **10 years** before the next starts (in the current zoom); closer or overlapping spans stack into extra rows (`assignTemporalLanes`). Lane choice **minimizes vertical branch length**: children prefer parent lanes (same row when years allow), corridor clearing (up to 20 passes) evicts unrelated streams preferring **rows above** the parent/child strip, then re-attracts the child toward the parent |
|
||||
| Branch connectors | Smooth curves from fan-out points along a parent stream to the **left edge (start)** of each child stream; origin X is always strictly left of the target (time-forward only — never right→left); stroke thickness matches the **target** (child) band height; color gradients from parent → child at constant opacity; a stream-shaped mask hides branch ink under movements so translucent overlaps do not brighten the bands; pan/zoom layout changes **animate** (streams + branches chase new geometry) so shifts stay trackable |
|
||||
| Band thickness | Stream height is **proportional to `influence_link_count`** (edges on paintings by artists in that movement): thicker bands for denser influence graphs; lane rows share vertical space weighted by the thickest band in each lane |
|
||||
@@ -296,7 +297,7 @@ Each artist appears as a **portrait circle** on their movement’s stream row:
|
||||
|
||||
Only **mousedown** on portraits and movement labels stops propagation (so drag-to-pan does not start when clicking them). Hovering a portrait highlights the artist’s lifespan on the era bar and brightens their segment on the movement stream.
|
||||
|
||||
**Note:** Movement lineage is **frontend curation** for layout and labels — it is not stored in PostgreSQL. Painting influence links live in **`painting_influence_sources`** (paintings, artists, or movements as sources). The API reads that table for detail panels, hall navigation, and `has_influence_links`. The legacy **`painting_influences`** table is still written in parallel when curators add painting-to-painting edges but is not queried for display.
|
||||
**Note:** Movement lineage lives in **`movement_lineage_links`** (source/target movements, approx year, required `proof_url`). Timeline / Tree of Art load it from `GET /api/movement-links` (static fallback in `movement-lineage.ts` if the API is empty). Curators with `influences` can add links and **vote to delete** (with reason); only **admins** hard-delete. Votes are stored in `movement_lineage_delete_votes`. Painting influence links live in **`painting_influence_sources`** (paintings, artists, or movements as sources). The API reads that table for detail panels, hall navigation, and `has_influence_links`. The legacy **`painting_influences`** table is still written in parallel when curators add painting-to-painting edges but is not queried for display.
|
||||
|
||||
## Virtual gallery (3D halls)
|
||||
|
||||
@@ -459,7 +460,7 @@ Next to the toggle, **Show more** (checkbox, persisted in `localStorage`) opens
|
||||
- **Timeline catalog** loads once from the API; pan/zoom is client-side only, with per-frame batching via `createViewChangeScheduler()`.
|
||||
- **Movement flow interaction** uses simplified SVG and hides portrait DOM during active scroll/drag so zoom stays responsive over dense portrait fields.
|
||||
- **Movement filtering** on zoom shows movements whose span overlaps the visible year range and that have at least one catalogued artist; a movement (e.g. Byzantine or Gothic viewed at 311–1231 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** — top-left **Movement links** opens `MovementLinksPage` (login if needed). Create requires `influences` + proof URL; curators vote to delete with a reason; admins review votes and delete. Data: `movement_lineage_links` / `movement_lineage_delete_votes`; timeline uses `GET /api/movement-links` with fallback `movement-lineage.ts`. After code changes that affect the SPA on port `3451`, run `npm run build --prefix client` (that port serves `client/dist`).
|
||||
- **One hall per artist** keeps navigation predictable: enter from the timeline or bio, leave via the single exit or back button.
|
||||
- **Movement galleries** complement artist halls: full movement corpus in period-themed wings, entered from the flow diagram.
|
||||
- **Guided tours** add curator-ordered winged halls with stop text on painting detail — [tours.md](tours.md).
|
||||
|
||||
@@ -263,18 +263,23 @@ Discovered rows are stored with `confidence: discovered` and `discovered_via` (e
|
||||
|
||||
Extend `art-influences-data.js` for high-quality curated chains; use discovery to suggest additional artist and movement links for manual review.
|
||||
|
||||
## Movement lineage (frontend flow diagram)
|
||||
## Movement lineage (timeline / Tree of Art)
|
||||
|
||||
Separate from the painting influence graph, `client/src/data/movement-lineage.ts` lists **art-movement** predecessor→successor pairs used only by `MovementBands.tsx` to position streams and draw branch connectors (e.g. Impressionism → Post-Impressionism → Fauvism / Cubism / Expressionism).
|
||||
Separate from the painting influence graph, **`movement_lineage_links`** stores art-movement predecessor→successor pairs used by `MovementBands`, `VerticalMovementBands`, and `MovementTree` (e.g. Impressionism → Post-Impressionism → Fauvism / Cubism / Expressionism).
|
||||
|
||||
| Aspect | Detail |
|
||||
|--------|--------|
|
||||
| Storage | TypeScript module in the client — **not** a database table |
|
||||
| Format | `[parentMovementName, childMovementName]` tuples; names must match `art_movements.name` from the seed |
|
||||
| Multiple parents | Allowed (e.g. Post-Impressionism feeding several modern paths) |
|
||||
| Sources | Curator notes in the file reference Met essays, TheArtStory, and similar |
|
||||
| Storage | PostgreSQL `movement_lineage_links` (`db/migrate-movement-links.sql`) |
|
||||
| Required proof | `proof_url` must be a valid `http(s)` URL (museum essay, literature, web reference) |
|
||||
| Optional | `link_year_approx`, `proof_label`, `notes` |
|
||||
| Format | FK pairs to `art_movements`; multiple parents allowed |
|
||||
| API | Public `GET /api/movement-links`; curator `POST` create + delete votes; **admin-only** hard `DELETE` |
|
||||
| UI | Top-left **Movement links** (timeline header); also linked from Influences. Curators create + vote-to-delete (reason); admins review votes and hard-delete |
|
||||
| Fallback | `client/src/data/movement-lineage.ts` if the API returns no rows |
|
||||
| Defaults | Seeded on migrate / first GET from Met / TheArtStory / Christie's essays |
|
||||
| Delete votes | `movement_lineage_delete_votes` (`db/migrate-movement-link-delete-votes.sql`); one vote per curator per link |
|
||||
|
||||
To add or fix a branch, edit `MOVEMENT_LINEAGE` in that file and rebuild the client. No migration or API change is required.
|
||||
To add a branch, use **Movement links** (proof URL required). To remove one, curators vote with a reason; an admin deletes after review. No client rebuild is required for data changes; rebuild `client/dist` only when shipping SPA changes to the API-static port (`3451` / prod).
|
||||
|
||||
## Movement gallery interiors (frontend)
|
||||
|
||||
|
||||
@@ -82,6 +82,23 @@ Audit: `influence.create` / `update` / `delete` / `import` in `curator_audit_log
|
||||
|
||||
---
|
||||
|
||||
## Movement lineage links (related curator tool)
|
||||
|
||||
Top-left timeline control **Movement links** (also from Influences → **Movement links →**). Create/vote need `influences`; hard delete is **admin-only**. Tables `movement_lineage_links` + `movement_lineage_delete_votes`.
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| `GET` | `/api/movement-links` | Public list (timeline / tree); includes `delete_vote_count` |
|
||||
| `POST` | `/api/movement-links` | Create (`sourceMovementId`, `targetMovementId`, `proofUrl` required) |
|
||||
| `POST` | `/api/movement-links/:id/delete-votes` | Curator vote to delete (`reason` required, min 10 chars) |
|
||||
| `GET` | `/api/movement-links/:id/delete-votes` | List votes (username + reason) for admin review |
|
||||
| `DELETE` | `/api/movement-links/:id/delete-votes/me` | Withdraw own vote |
|
||||
| `DELETE` | `/api/movement-links/:id` | **Admin only** — hard delete |
|
||||
|
||||
See [data-and-images.md](data-and-images.md#movement-lineage-timeline--tree-of-art).
|
||||
|
||||
---
|
||||
|
||||
## CLI still available
|
||||
|
||||
- `npm run dev:update-influences` — curated [`scripts/art-influences-data.js`](../scripts/art-influences-data.js)
|
||||
|
||||
@@ -102,6 +102,8 @@ All constants sit at the top of the two files and are safe to tune:
|
||||
| `FULL_VIEW_WIDTH_SHARE`, `ZOOM_SPREAD_EXPONENT`, `MAX_FIT_BOOST` | `MovementTree.tsx` | crown spread vs. zoom |
|
||||
| `MIN_LIMB_RISE_PX`, `MIN_JUNCTION_RISE_PX`, `MAX_THICKNESS_OF_LENGTH` | `MovementTree.tsx` | crown legibility at full zoom-out |
|
||||
|
||||
New lineage edges only need adding to
|
||||
[`client/src/data/movement-lineage.ts`](../client/src/data/movement-lineage.ts) — the tree
|
||||
picks up parents, grafts, thickness and spacing from there automatically.
|
||||
New lineage edges: top-left **Movement links** (required proof URL) →
|
||||
`movement_lineage_links` / `GET /api/movement-links`. Curators vote to delete;
|
||||
admins hard-delete. Static fallback remains in
|
||||
[`client/src/data/movement-lineage.ts`](../client/src/data/movement-lineage.ts).
|
||||
The tree picks up parents, grafts, thickness and spacing from the active lineage automatically.
|
||||
@@ -72,7 +72,7 @@ If migration fails with permission errors, grant schema rights to the app user f
|
||||
|
||||
`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, Tour editor, and all mutating debug APIs require an active curator session. Anonymous visitors browse the timeline, published Tours, and 3D halls without logging in.
|
||||
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](API.md#authentication) and [basics.md — Developer tools](basics.md#developer-tools-image-audit).
|
||||
|
||||
|
||||
Reference in new issue
Block a user