From f78c14f307f493aba6daa28735db6614883e2c82 Mon Sep 17 00:00:00 2001 From: Danila Khodjaef Date: Thu, 9 Jul 2026 18:30:19 +0300 Subject: [PATCH] Fix debug upload persistence and UX; exclude prod audit log from DB restore Uploads and fixes now bust browser cache via file-mtime keys in API responses. Debug upload shows a centered loading overlay and blocks search while uploading. Prod DB restore skips curator_audit_log. Co-authored-by: Cursor --- Documentation/API.md | 31 ++++- Documentation/basics.md | 4 +- Documentation/data-and-images.md | 6 +- Documentation/deploy-dev-to-prod.md | 2 +- Documentation/setup.md | 4 +- client/src/api/client.ts | 131 ++++++++++++++---- client/src/components/ArtistBio.css | 1 + client/src/components/ArtistBio.tsx | 90 +++++++----- client/src/components/DebugUploadButton.tsx | 57 ++++++++ .../src/components/GalleryLoadingMarker.css | 16 +++ .../src/components/GalleryLoadingMarker.tsx | 7 +- client/src/components/PaintingDetail.css | 45 +++++- client/src/components/PaintingDetail.tsx | 113 ++++++++------- client/src/pages/HomePage.tsx | 5 + client/src/types/index.ts | 4 + scripts/restore-db-data.js | 51 ++++++- server/image-service.js | 75 +++++++++- server/index.js | 18 +-- 18 files changed, 506 insertions(+), 154 deletions(-) create mode 100644 client/src/components/DebugUploadButton.tsx diff --git a/Documentation/API.md b/Documentation/API.md index 2baf037..7bb35b2 100644 --- a/Documentation/API.md +++ b/Documentation/API.md @@ -17,6 +17,8 @@ All JSON responses use `Content-Type: application/json`. Errors return `{ "error Static images are served at `/images/` from `IMAGE_DIR`. +**Caching:** `/images` responses use `Cache-Control: public, max-age=0, must-revalidate` with `ETag` / `Last-Modified`. Painting and artist JSON payloads include optional **`image_cache_key`** / **`thumbnail_cache_key`** (and **`portrait_cache_key`** / **`portrait_thumb_cache_key`** on artists) — Unix ms from the file’s `mtime` on disk. The client appends `?v=` to image URLs so fix/upload/clear updates show immediately after reload even when the relative path is unchanged. + **Quick check:** ```powershell @@ -440,7 +442,18 @@ Painting detail with influence graph neighbours. ```json { - "painting": { "id": 10, "title": "...", "artist_name": "...", "image_path": "...", "checkup_checked": false, "checkup_fixed": false, "has_influence_links": true, ... }, + "painting": { + "id": 10, + "title": "...", + "artist_name": "...", + "image_path": "paintings/Artist_Title.jpg", + "thumbnail_path": "paintings/thumbs/Artist_Title_thumb.jpg", + "image_cache_key": 1739123456789, + "thumbnail_cache_key": 1739123456790, + "checkup_checked": false, + "checkup_fixed": false, + "has_influence_links": true + }, "influencedBy": [ { "source_type": "painting", @@ -600,6 +613,8 @@ Only `imageUrl` is required; optional fields improve fetch success for hotlinked { "imagePath": "paintings/Artist_Title.jpg", "thumbnailPath": "paintings/thumbs/Artist_Title_thumb.jpg", + "image_cache_key": 1739123456789, + "thumbnail_cache_key": 1739123456790, "fixed": true, "checked": true } @@ -660,7 +675,7 @@ Upload a local painting image (base64 JSON body). Validates with `sharp`, writes } ``` -Max decoded size 15 MB (JSON body limit 20 MB on the server). **Response** — same shape as `fix-image` (`imagePath`, `thumbnailPath`, `fixed`, `checked`). +Max decoded size 15 MB (JSON body limit 20 MB on the server). **Response** — same shape as `fix-image` (`imagePath`, `thumbnailPath`, `image_cache_key`, `thumbnail_cache_key`, `fixed`, `checked`). --- @@ -711,11 +726,13 @@ The React client wraps these endpoints in `client/src/api/client.ts`. All reques | `api.getArtistNavigation(id)` | `GET /api/artists/:id/navigation` | | `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) | -| `galleryImageUrlWithRevision(painting, revision)` | Local URL with `?v=` cache buster after fix | -| `paintingImageUrl(painting)` | Local file, on-demand API, or `null` when cleared (`checkup_fixed` + no paths) | -| `portraitUrl(path, revision?)` | `/images/` with optional `?v=` cache buster | +| `imageUrl(path, revision?)` | `/images/` or placeholder; optional `?v=` cache buster | +| `paintingImageRevision(painting, sessionRevision?)` | Prefer API `image_cache_key` / `thumbnail_cache_key`, else in-session counter | +| `galleryImageUrl(painting, sessionRevision?)` | Local thumb/full only (3D); auto `?v=` from cache keys | +| `galleryImageUrlWithRevision(painting, sessionRevision?)` | Alias of `galleryImageUrl` | +| `paintingImageUrl(painting, sessionRevision?)` | Local file, on-demand API, or `null` when cleared (`checkup_fixed` + no paths) | +| `portraitUrl(path, revision?, artist?)` | `/images/` with `?v=` from revision or artist cache keys | +| `validateDebugUploadFile(file)` | Client-side size/type check before base64 upload | | `api.getPaintingCheckup()` | `GET /api/paintings/checkup` | | `api.updatePaintingCheckupFlags(id, flags)` | `PATCH /api/paintings/:id/checkup-flags` | | `api.getPaintingDebugImageSearch(id)` | `GET /api/paintings/:id/debug-image-search` | diff --git a/Documentation/basics.md b/Documentation/basics.md index 7921151..990111e 100644 --- a/Documentation/basics.md +++ b/Documentation/basics.md @@ -405,11 +405,13 @@ When debug mode is on, a panel at the bottom-left shows the image search query, | **Fix it** | Replaces full image from search result; **regenerates painting thumb** (~400px JPEG) from that file | Replaces portrait; **regenerates timeline thumb** (256px) | | **More** | Modal with up to **20** results (resolution shown when known); thumb regenerated from chosen full image | Same | | **Clear** | Deletes files, clears DB paths, empty frame | Clears portrait slot | -| **Upload** | Local file picker → full image + **auto-generated painting thumb** | Local file → portrait + **auto-generated portrait thumb** | +| **Upload** | Local file picker (`DebugUploadButton`) → full image + **auto-generated painting thumb**; full-page **Loading…** overlay; hides current image and pauses search/fix until upload finishes | Local file → portrait + **auto-generated portrait thumb**; same upload overlay behaviour | | **Remove entry** | **Painting detail only** — deletes row from DB, removes image files, refreshes 3D gallery, navigates to next/previous work in catalog (or back to gallery if last work). No confirmation dialog. | — | After **Fix it**, **More**, **Upload**, or **Clear**, the main view, gallery textures (paintings), and timeline portrait (artists) update without a full page reload. Painting and portrait thumbs under `data/images/*/thumbs/` are rebuilt on the server whenever a curator replaces the full image. **Remove entry** refetches artist (and movement gallery when relevant) from the API and remounts the 3D hall so the deleted frame disappears immediately. +Pressing **Upload** clears the debug search preview and closes **More** before the file picker opens. While uploading, **Fix it**, **More**, and **Checked** are disabled and the main painting/portrait is hidden behind a centered loading overlay. + Reviewed portraits show a gold border on the bio page; reviewed paintings use gold frames in the 3D hall. **Back to Gallery** returns to the live hall session, not a stale snapshot. Influence side-panel thumbnails use **letterboxing** (`object-fit: contain`) so full compositions are visible. diff --git a/Documentation/data-and-images.md b/Documentation/data-and-images.md index b899d38..7d0a910 100644 --- a/Documentation/data-and-images.md +++ b/Documentation/data-and-images.md @@ -481,7 +481,7 @@ When **Debug mode** is on (home header) or from the **Checkup** page: 2. **More** — `GET …/debug-image-search/more` or `…/debug-portrait-search/more` returns up to 20 ranked candidates (`searchPaintingImagesMany` / `searchArtistPortraitMany`). The modal shows each thumbnail with **resolution** when the search API provides dimensions; otherwise the client probes via `GET /api/debug/image-proxy`. 3. **Fix** — `POST …/fix-image` or `…/fix-portrait` downloads the chosen URL via `downloadImageForFix` → `replacePaintingImageFromUrl` / `replaceArtistPortraitFromUrl` in `server/image-service.js`. The server **always regenerates thumbnails from the saved full image** (`writePaintingThumb` / `writePortraitThumb` via `sharp` — not the search-result thumb URL), updates `thumbnail_path` / `portrait_thumb_path`, and sets `checkup_fixed` + `checkup_checked`. 4. **Clear** — `POST …/clear-image` or `…/clear-portrait` deletes local file(s), nulls DB paths, sets both flags. Cleared slots stay empty in the UI (no placeholder; `checkup_fixed` prevents on-demand refetch for paintings). -5. **Upload** — `POST …/upload-image` or `…/upload-portrait` accepts a base64-encoded file in JSON (Express body limit **20 MB**; decoded image max **15 MB**), validates with `sharp`, writes to the standard filename under `data/images/`, and regenerates the matching thumbnail the same way as **Fix it**. +5. **Upload** — `POST …/upload-image` or `…/upload-portrait` accepts a base64-encoded file in JSON (Express body limit **20 MB**; decoded image max **15 MB**), validates with `sharp`, writes to the standard filename under `data/images/`, and regenerates the matching thumbnail the same way as **Fix it**. The file picker uses a native `