Files
picpeak/frontend/src/services/uploads.service.ts
T
Paul Nothaft 3b827b80d5 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.
2026-05-02 22:56:04 +02:00

58 lines
1.8 KiB
TypeScript

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`;
},
};