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 <cursoragent@cursor.com>
This commit is contained in:
Danila KhodjaefandCursor committed 2026-07-09 18:30:19 +03:00
1 parent 62096e8210
commit f78c14f307
18 files changed
+506 -154

No files matched your search

+24 -7
View File
@@ -17,6 +17,8 @@ All JSON responses use `Content-Type: application/json`. Errors return `{ "error
Static images are served at `/images/<relative-path>` 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=<key>` 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/<path>` 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/<path>` with optional `?v=` cache buster |
| `imageUrl(path, revision?)` | `/images/<path>` 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/<path>` 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` |
+3 -1
View File
@@ -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.
+3 -3
View File
@@ -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 `<label>` + hidden `<input>` (`DebugUploadButton.tsx`) so the `change` event is reliable on Windows/Chromium.
6. **Remove entry** (painting detail only) — `DELETE /api/paintings/:id` via `deletePainting()` in `server/image-service.js`: deletes image files, removes the DB row (cascade on influence/annotation tables), refetches artist/movement gallery data, remounts the 3D hall, and navigates to the next or previous catalog work with no confirmation dialog.
### Debug panel (painting detail and artist bio)
@@ -494,10 +494,10 @@ With debug mode on, `PaintingDetail.tsx` and `ArtistBio.tsx` show a bottom-left
| **Fix it** | `POST …/fix-image` / `…/fix-portrait` | Saves top search result to disk, **regenerates thumb from full image**, sets both flags, refreshes detail + gallery / timeline |
| **More** | `GET …/debug-*-search/more` then fix endpoint | Modal with 20 clickable results (resolution label under each thumb); pick one to replace (thumb regenerated from downloaded full) |
| **Clear** | `POST …/clear-image` / `…/clear-portrait` | Removes file(s), empty frame in UI |
| **Upload** | `POST …/upload-image` / `…/upload-portrait` | Local file picker → save full image + **auto-generated thumb** |
| **Upload** | `POST …/upload-image` / `…/upload-portrait` | Local file picker (`DebugUploadButton`) → save full image + **auto-generated thumb**; centered **Loading…** overlay; clears preview and blocks search/fix while uploading |
| **Remove entry** | `DELETE /api/paintings/:id` | **Paintings only** — permanent delete + gallery refresh + catalog navigation |
The client passes `searchUrl`, `source`, and `thumbUrl` from search results to improve download reliability. After a fix, clear, upload, or remove, `HomePage` updates the gallery session and appends a revision query on texture URLs so replaced files reload even when the path is unchanged.
The client passes `searchUrl`, `source`, and `thumbUrl` from search results to improve download reliability. After a fix, clear, upload, or remove, `HomePage` updates the gallery session. Image URLs include `?v=<mtime_ms>` from API **`image_cache_key`** / **`thumbnail_cache_key`** (file `mtime` on disk) so replaced files reload after a full page refresh even when the relative path is unchanged. In-session counters still bump immediately after a mutation.
Checkup **Search visible** queues search for filtered rows only (3 concurrent); it does not search the full catalog on load.
+1 -1
View File
@@ -242,7 +242,7 @@ Multi-line values (e.g. artist bios with embedded newlines) are parsed as whole
**Caveats — prod tables are replaced by dev's contents:**
- `users` and `curator_audit_log` are overwritten. The **dev curator account and password become the prod login**, and prod audit history is replaced. Make sure the dev curator credentials are the ones you want in prod.
- `users` is overwritten. The **dev curator account and password become the prod login**. `curator_audit_log` is **not** synced — prod keeps its existing audit history.
- The `session` table is truncated, so any active prod curator sessions are logged out.
- The target is guarded: the restore refuses to run unless the database name ends with `_prod` and only reads `infra/docker/.env.prod`.
+3 -1
View File
@@ -258,9 +258,11 @@ After clone: copy `.env.example` → `.env`, install dependencies, run [one-time
| 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 updates session + `?v=` revision — use **Back to Gallery** (not browser back) |
| 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) |
| 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 |