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.