feat(upload): async photo processing — frontend (PR-B part 2)
Live processing-state UI that complements the backend async pipeline.
Modal stays open through the processing phase and surfaces real
progress (X of N photos processed); the admin grid renders placeholder
cards for in-flight photos and auto-refreshes via polling until the
queue drains.
services/uploads.service.ts (new)
- getStatus(uploadId) — JSON snapshot from /admin/uploads/:id/status
- retryPhoto(photoId) — POST /admin/photos/:id/retry
- streamUrl(uploadId) — SSE upgrade URL
hooks/useUploadProgress.ts (new)
- Tracks N concurrent upload IDs (one per chunk POST) and merges
counters into a single aggregate.
- Always polls every 1.5s; opportunistic SSE upgrade on top of that.
SSE failure (proxy buffering, etc.) silently downgrades to polling
only — no reconnect storms.
- Auto-stops both channels when every tracked group is in a terminal
(complete/failed) state.
components/admin/PhotoUpload.tsx
- Captures upload_id from each chunk's 202 response, feeds them into
useUploadProgress.
- Phase machine extended: stays in 'processing' until the worker
drains the queue (not just until bytes-on-wire). Progress UI shows
real "X of N done" with a determinate bar fed by the aggregate.
- "You can leave this page" hint kept — closing the modal is now
actually safe, work continues server-side.
- Side-effect refactor: invokes onUploadComplete twice — once early
so the user sees photos appearing immediately, once on terminal
so the parent grid sees final state.
components/admin/AdminPhotoGrid.tsx
- Photos with processing_status pending/processing render an amber
placeholder card with a spinning Cog instead of the missing
thumbnail.
- Photos with status='failed' render a red card with the error message
and a "Retry" button that POSTs /admin/photos/:id/retry.
pages/admin/EventDetailsPage.tsx
- Photo list query gains refetchInterval that polls every 2s while
any photo is non-terminal, then stops. Keeps the grid auto-fresh
during ongoing processing.
This commit is contained in:
@@ -302,11 +302,22 @@ export const EventDetailsPage: React.FC = () => {
|
||||
logic: feedbackFilters.logic,
|
||||
}), [photoFilters, feedbackFilters]);
|
||||
|
||||
// Fetch photos (needed for both photos tab and hero photo selector)
|
||||
// Fetch photos (needed for both photos tab and hero photo selector).
|
||||
// While any photo is still in pending/processing state we poll every
|
||||
// 2s so the admin grid auto-updates as the background worker drains
|
||||
// the queue. Once everything is complete/failed the polling stops.
|
||||
const { data: photos = [], isLoading: photosLoading, refetch: refetchPhotos } = useQuery({
|
||||
queryKey: ['admin-event-photos', id, combinedPhotoFilters],
|
||||
queryFn: () => photosService.getEventPhotos(parseInt(id!), combinedPhotoFilters),
|
||||
enabled: !!id && (activeTab === 'photos' || isEditing),
|
||||
refetchInterval: (query) => {
|
||||
const data = query.state.data as AdminPhoto[] | undefined;
|
||||
if (!Array.isArray(data)) return false;
|
||||
const inFlight = data.some(
|
||||
(p: any) => p.processing_status === 'pending' || p.processing_status === 'processing'
|
||||
);
|
||||
return inFlight ? 2000 : false;
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch filter summary for feedback filters
|
||||
|
||||
Reference in New Issue
Block a user