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:
@@ -0,0 +1,57 @@
|
||||
import { api } from '../config/api';
|
||||
|
||||
export type PhotoProcessingStatus = 'pending' | 'processing' | 'complete' | 'failed';
|
||||
|
||||
export interface UploadPhotoStatus {
|
||||
id: number;
|
||||
filename: string;
|
||||
original_filename: string;
|
||||
status: PhotoProcessingStatus;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface UploadStatusSnapshot {
|
||||
upload_id: string;
|
||||
event_id: number;
|
||||
total: number;
|
||||
pending: number;
|
||||
processing: number;
|
||||
complete: number;
|
||||
failed: number;
|
||||
photos: UploadPhotoStatus[];
|
||||
}
|
||||
|
||||
export const uploadsService = {
|
||||
/**
|
||||
* One-shot snapshot of an upload group's processing state. Frontends
|
||||
* poll this every 1.5s while any photo is still pending/processing.
|
||||
*/
|
||||
async getStatus(uploadId: string): Promise<UploadStatusSnapshot> {
|
||||
const response = await api.get<UploadStatusSnapshot>(`/admin/uploads/${uploadId}/status`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retry a failed photo. Flips status back to 'pending' so the
|
||||
* background worker picks it up again.
|
||||
*/
|
||||
async retryPhoto(photoId: number): Promise<{ id: number; status: PhotoProcessingStatus }> {
|
||||
const response = await api.post<{ id: number; status: PhotoProcessingStatus }>(
|
||||
`/admin/photos/${photoId}/retry`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Build the SSE stream URL for an upload group. Caller is responsible
|
||||
* for opening an EventSource and merging the JSON-payload events into
|
||||
* their progress state. Falls back to polling getStatus() if the
|
||||
* EventSource fails to open (proxy buffering, etc.).
|
||||
*/
|
||||
streamUrl(uploadId: string): string {
|
||||
// EventSource doesn't send our auth headers, so we have to rely on
|
||||
// the cookie-based admin session. (PicPeak's auth middleware reads
|
||||
// cookies before falling back to Authorization headers.)
|
||||
return `${api.defaults.baseURL || ''}/admin/uploads/${uploadId}/stream`;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user