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:
co-authored by
Cursor
parent
62096e8210
commit
f78c14f307
@@ -1,10 +1,13 @@
|
||||
import { useCallback, useEffect, useRef, useState, type ChangeEvent, type SyntheticEvent } from 'react';
|
||||
import { useCallback, useEffect, useState, type SyntheticEvent } from 'react';
|
||||
import type { InfluenceLink, Painting, PaintingDetail } from '../types';
|
||||
import { api, debugImageProxyUrl, imageUrl, paintingImageUrl, type DebugImageSearchResult, type DebugImageSearchResultItem, type FixPaintingImageResult } from '../api/client';
|
||||
import DebugSearchResultsModal from './DebugSearchResultsModal';
|
||||
import DebugUploadButton from './DebugUploadButton';
|
||||
import GalleryLoadingMarker from './GalleryLoadingMarker';
|
||||
import PaintingAnnotationsPanel, { PaintingAnnotationMarkers } from './PaintingAnnotations';
|
||||
import PaintingLightbox from './PaintingLightbox';
|
||||
import './PaintingDetail.css';
|
||||
import './GalleryLoadingMarker.css';
|
||||
|
||||
interface Props {
|
||||
data: PaintingDetail;
|
||||
@@ -214,15 +217,13 @@ export default function PaintingDetailView({
|
||||
const [applyingUrl, setApplyingUrl] = useState<string | null>(null);
|
||||
const [clearing, setClearing] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [uploadStatus, setUploadStatus] = useState<string | null>(null);
|
||||
const [removing, setRemoving] = useState(false);
|
||||
const [activeAnnotationId, setActiveAnnotationId] = useState<number | null>(null);
|
||||
const uploadInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const baseImageUrl = paintingImageUrl(painting);
|
||||
const imageSrc = baseImageUrl
|
||||
? `${baseImageUrl}${baseImageUrl.includes('?') ? '&' : '?'}v=${imageVersion}`
|
||||
: null;
|
||||
const imageCleared = !baseImageUrl && !!painting.checkup_fixed;
|
||||
const imageSrc = paintingImageUrl(painting, imageVersion || undefined);
|
||||
const displayImageSrc = uploading ? null : imageSrc;
|
||||
const imageCleared = !painting.image_path && !painting.thumbnail_path && !!painting.checkup_fixed;
|
||||
|
||||
const catalogIndex = artistPaintings.findIndex((p) => p.id === painting.id);
|
||||
const previousPainting = catalogIndex > 0 ? artistPaintings[catalogIndex - 1] : null;
|
||||
@@ -243,6 +244,7 @@ export default function PaintingDetailView({
|
||||
setFixing(false);
|
||||
setClearing(false);
|
||||
setUploading(false);
|
||||
setUploadStatus(null);
|
||||
setMarkingChecked(false);
|
||||
setApplyingUrl(null);
|
||||
setMoreLoading(false);
|
||||
@@ -255,6 +257,9 @@ export default function PaintingDetailView({
|
||||
setMoreOpen(false);
|
||||
return;
|
||||
}
|
||||
if (uploading) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setDebugLoading(true);
|
||||
@@ -275,7 +280,7 @@ export default function PaintingDetailView({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [debugMode, painting.id, painting.title, painting.artist_name]);
|
||||
}, [debugMode, uploading, painting.id, painting.title, painting.artist_name]);
|
||||
|
||||
const applyImageUpdate = async (fixResult: FixPaintingImageResult) => {
|
||||
setImageVersion((v) => v + 1);
|
||||
@@ -382,20 +387,27 @@ export default function PaintingDetailView({
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadClick = () => {
|
||||
uploadInputRef.current?.click();
|
||||
const handleUploadPress = () => {
|
||||
setDebugSearch(null);
|
||||
setDebugLoading(false);
|
||||
setMoreOpen(false);
|
||||
setMoreResults(null);
|
||||
setMoreError(null);
|
||||
setDebugError(null);
|
||||
};
|
||||
|
||||
const handleUploadFile = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
e.target.value = '';
|
||||
if (!file || uploading || fixing || clearing) return;
|
||||
const handleUploadFile = async (file: File) => {
|
||||
if (uploading || fixing || clearing) return;
|
||||
handleUploadPress();
|
||||
setUploading(true);
|
||||
setDebugError(null);
|
||||
setUploadStatus('Reading file…');
|
||||
try {
|
||||
setUploadStatus('Uploading…');
|
||||
const result = await api.uploadPaintingImage(painting.id, file);
|
||||
await applyImageUpdate(result);
|
||||
setUploadStatus('Upload complete.');
|
||||
} catch (err) {
|
||||
setUploadStatus(null);
|
||||
setDebugError(err instanceof Error ? err.message : 'Could not upload image.');
|
||||
} finally {
|
||||
setUploading(false);
|
||||
@@ -437,6 +449,13 @@ export default function PaintingDetailView({
|
||||
|
||||
return (
|
||||
<div className={`painting-detail${fullscreen ? ' painting-detail-fullscreen-active' : ''}`}>
|
||||
{uploading && (
|
||||
<GalleryLoadingMarker
|
||||
overlay
|
||||
className="debug-upload-page-overlay"
|
||||
message={uploadStatus ?? 'Loading…'}
|
||||
/>
|
||||
)}
|
||||
<header className="painting-header">
|
||||
<button className="back-btn" onClick={onBack}>← Back to Gallery</button>
|
||||
<div className="painting-title-block">
|
||||
@@ -494,12 +513,12 @@ export default function PaintingDetailView({
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`painting-frame-large${imageSrc ? ' painting-frame-clickable' : ' painting-frame-empty'}${imageCleared ? ' painting-frame-cleared' : ''}`}
|
||||
role={imageSrc ? 'button' : undefined}
|
||||
tabIndex={imageSrc ? 0 : undefined}
|
||||
onClick={imageSrc ? () => setFullscreen(true) : undefined}
|
||||
className={`painting-frame-large${displayImageSrc ? ' painting-frame-clickable' : ' painting-frame-empty'}${imageCleared && !uploading ? ' painting-frame-cleared' : ''}${uploading ? ' painting-frame-uploading' : ''}`}
|
||||
role={displayImageSrc ? 'button' : undefined}
|
||||
tabIndex={displayImageSrc ? 0 : undefined}
|
||||
onClick={displayImageSrc ? () => setFullscreen(true) : undefined}
|
||||
onKeyDown={
|
||||
imageSrc
|
||||
displayImageSrc
|
||||
? (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
@@ -508,14 +527,14 @@ export default function PaintingDetailView({
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
title={imageSrc ? 'View full screen' : undefined}
|
||||
aria-label={imageSrc ? `View ${painting.title} full screen` : `${painting.title} (no image)`}
|
||||
title={displayImageSrc ? 'View full screen' : undefined}
|
||||
aria-label={displayImageSrc ? `View ${painting.title} full screen` : `${painting.title} (no image)`}
|
||||
>
|
||||
<div className="painting-frame-image-wrap">
|
||||
{imageSrc ? (
|
||||
<img src={imageSrc} alt={painting.title} onError={handleImageError} draggable={false} />
|
||||
{displayImageSrc ? (
|
||||
<img src={displayImageSrc} alt={painting.title} onError={handleImageError} draggable={false} />
|
||||
) : null}
|
||||
{imageSrc && annotations.length > 0 && (
|
||||
{displayImageSrc && annotations.length > 0 && (
|
||||
<PaintingAnnotationMarkers
|
||||
annotations={annotations}
|
||||
activeId={activeAnnotationId}
|
||||
@@ -573,14 +592,17 @@ export default function PaintingDetailView({
|
||||
</div>
|
||||
|
||||
{debugMode && (
|
||||
<aside className="debug-image-panel" aria-label="Debug image search">
|
||||
<aside
|
||||
className={`debug-image-panel${uploading ? ' debug-image-panel-blocked' : ''}`}
|
||||
aria-label="Debug image search"
|
||||
>
|
||||
<h4>{debugSearch?.sourceLabel ?? 'Google image search'}</h4>
|
||||
<p className="debug-image-query">
|
||||
{debugSearch?.query ?? `${painting.artist_name} ${painting.title} painting`}
|
||||
</p>
|
||||
{debugLoading && <p className="debug-image-status">Searching…</p>}
|
||||
{debugError && <p className="debug-image-error">{debugError}</p>}
|
||||
{!debugLoading && debugSearch?.imageUrl && (
|
||||
{debugLoading && !uploading && <p className="debug-image-status">Searching…</p>}
|
||||
{debugError && !uploading && <p className="debug-image-error">{debugError}</p>}
|
||||
{!uploading && !debugLoading && debugSearch?.imageUrl && (
|
||||
<img
|
||||
className="debug-image-preview"
|
||||
src={debugImageProxyUrl(debugSearch.imageUrl, {
|
||||
@@ -593,7 +615,7 @@ export default function PaintingDetailView({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!debugLoading && debugSearch && !debugSearch.imageUrl && !debugError && (
|
||||
{!uploading && !debugLoading && debugSearch && !debugSearch.imageUrl && !debugError && (
|
||||
<p className="debug-image-status">No Google image result found.</p>
|
||||
)}
|
||||
<div className="debug-action-buttons">
|
||||
@@ -601,7 +623,7 @@ export default function PaintingDetailView({
|
||||
type="button"
|
||||
className="debug-checked-btn"
|
||||
onClick={handleMarkChecked}
|
||||
disabled={!!painting.checkup_checked || markingChecked}
|
||||
disabled={!!painting.checkup_checked || markingChecked || uploading}
|
||||
>
|
||||
{markingChecked ? '…' : 'Checked'}
|
||||
</button>
|
||||
@@ -609,7 +631,7 @@ export default function PaintingDetailView({
|
||||
type="button"
|
||||
className="debug-fix-btn"
|
||||
onClick={handleFixImage}
|
||||
disabled={fixing || debugLoading || !debugSearch?.imageUrl}
|
||||
disabled={fixing || debugLoading || uploading || !debugSearch?.imageUrl}
|
||||
>
|
||||
{fixing ? '…' : 'Fix it'}
|
||||
</button>
|
||||
@@ -617,7 +639,7 @@ export default function PaintingDetailView({
|
||||
type="button"
|
||||
className="debug-more-btn"
|
||||
onClick={handleOpenMore}
|
||||
disabled={debugLoading || moreLoading}
|
||||
disabled={debugLoading || moreLoading || uploading}
|
||||
>
|
||||
{moreLoading ? '…' : 'More'}
|
||||
</button>
|
||||
@@ -631,20 +653,11 @@ export default function PaintingDetailView({
|
||||
>
|
||||
{clearing ? '…' : 'Clear'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="debug-upload-btn"
|
||||
onClick={handleUploadClick}
|
||||
disabled={uploading || fixing || clearing}
|
||||
>
|
||||
{uploading ? '…' : 'Upload'}
|
||||
</button>
|
||||
<input
|
||||
ref={uploadInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
hidden
|
||||
onChange={handleUploadFile}
|
||||
<DebugUploadButton
|
||||
uploading={uploading}
|
||||
disabled={fixing || clearing}
|
||||
onUploadPress={handleUploadPress}
|
||||
onFileSelected={handleUploadFile}
|
||||
/>
|
||||
</div>
|
||||
<div className="debug-action-buttons debug-action-buttons-danger">
|
||||
@@ -661,7 +674,7 @@ export default function PaintingDetailView({
|
||||
)}
|
||||
|
||||
<DebugSearchResultsModal
|
||||
open={moreOpen}
|
||||
open={moreOpen && !uploading}
|
||||
title="Choose painting image"
|
||||
data={moreResults}
|
||||
loading={moreLoading}
|
||||
@@ -671,9 +684,9 @@ export default function PaintingDetailView({
|
||||
onSelect={handleSelectMoreResult}
|
||||
/>
|
||||
|
||||
{fullscreen && imageSrc && (
|
||||
{fullscreen && displayImageSrc && (
|
||||
<PaintingLightbox
|
||||
src={imageSrc}
|
||||
src={displayImageSrc}
|
||||
alt={painting.title}
|
||||
title={painting.title}
|
||||
subtitle={[painting.artist_name, painting.year ? String(painting.year) : '']
|
||||
|
||||
Reference in New Issue
Block a user