From b119951f93d951ff3cc3587819cfeb169a0b8255 Mon Sep 17 00:00:00 2001 From: michael Date: Sat, 24 Jan 2026 07:01:58 -0600 Subject: [PATCH] Fix retry button for failed proofs and hook up download asset button - Add GET /files/{storage_key:path} endpoint to serve stored files - Add getFile() method to apiService to fetch files from backend - Update convertProofToFrontend() to preserve fileStorageKey - Update handleRetryAnalysis() to fetch file from backend when not in memory - Update handleDownload() to download original file instead of thumbnail After page refresh, the retry button now fetches the original file from backend storage using the fileStorageKey, allowing failed proofs to be reprocessed. The Download Asset button also now downloads the original uploaded file rather than the preview thumbnail. Co-Authored-By: Claude Opus 4.5 --- backend/app/api/routes.py | 49 +++++++++++++++++++++++++++++++ frontend/App.tsx | 24 +++++++++++++-- frontend/components/Campaigns.tsx | 20 ++++++++++++- frontend/services/apiService.ts | 26 ++++++++++++++++ 4 files changed, 115 insertions(+), 4 deletions(-) diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py index 184b3b8..9aeb3f4 100755 --- a/backend/app/api/routes.py +++ b/backend/app/api/routes.py @@ -3,6 +3,7 @@ import uuid from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, Form +from fastapi.responses import Response from sqlalchemy.ext.asyncio import AsyncSession from app.api.schemas import ( @@ -21,6 +22,7 @@ from app.api.schemas import ( DropdownOptionsResponse, AgencyResponse, UserResponse, + SupportEmailRequest, ) from app.dependencies.auth import get_current_user from app.models.database import get_db @@ -32,6 +34,7 @@ from app.repositories import ( DropdownRepository, ) from app.services.storage_service import storage_service +from app.services.email_service import email_service router = APIRouter() @@ -640,3 +643,49 @@ async def list_agencies( agencies = result.scalars().all() return [AgencyResponse(id=a.id, name=a.name) for a in agencies] + + +# File download endpoint +@router.get("/files/{storage_key:path}") +async def get_file( + storage_key: str, + db: AsyncSession = Depends(get_db), + user: dict = Depends(get_current_user), +): + """Retrieve a stored file by its storage key.""" + file_data = await storage_service.get_file(storage_key) + if file_data is None: + raise HTTPException(status_code=404, detail="File not found") + + # Determine content type from extension + extension = storage_key.split('.')[-1].lower() if '.' in storage_key else '' + content_types = { + 'png': 'image/png', + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'gif': 'image/gif', + 'webp': 'image/webp', + 'svg': 'image/svg+xml', + 'pdf': 'application/pdf', + } + return Response( + content=file_data, + media_type=content_types.get(extension, 'application/octet-stream'), + ) + + +# Support email endpoint (public - no auth required for login page access) +@router.post("/support/email") +async def send_support_email( + data: SupportEmailRequest, +): + """Send support email - no auth required (for login page).""" + success = await email_service.send_support_email( + message=data.message, + subject=data.subject, + user_name=data.user_name, + user_email=data.user_email, + ) + if not success: + raise HTTPException(status_code=500, detail="Failed to send email") + return {"success": True, "message": "Email sent successfully"} diff --git a/frontend/App.tsx b/frontend/App.tsx index 57e960a..8862eaa 100755 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -286,8 +286,8 @@ const App: React.FC = () => { const handleRetryAnalysis = async (campaignName: string, tempId: string) => { const proofToRetry = campaignProofs[campaignName]?.find(proof => proof.tempId === tempId); - if (!proofToRetry || !proofToRetry.file) { - console.error("Proof to retry not found or file is missing"); + if (!proofToRetry) { + console.error("Proof to retry not found"); return; } @@ -298,7 +298,25 @@ const App: React.FC = () => { return; } - const { file, proofName, channel, subChannel, proofType } = proofToRetry; + let file = proofToRetry.file; + + // Fetch from backend if not in memory + if (!file) { + const storageKey = proofToRetry.fileStorageKey || proofToRetry.versions?.[0]?.fileStorageKey; + if (!storageKey) { + setError('Cannot retry: original file is not available.'); + return; + } + try { + file = await apiService.getFile(storageKey); + } catch (err) { + console.error("Failed to fetch file for retry:", err); + setError('Failed to retrieve original file for retry.'); + return; + } + } + + const { proofName, channel, subChannel, proofType } = proofToRetry; setCampaignProofs(prevProofs => ({ ...prevProofs, diff --git a/frontend/components/Campaigns.tsx b/frontend/components/Campaigns.tsx index 625c683..560817a 100755 --- a/frontend/components/Campaigns.tsx +++ b/frontend/components/Campaigns.tsx @@ -24,6 +24,7 @@ import { PDFIcon } from './icons/PDFIcon'; import { PDFReport } from './PDFReport'; import { ExportIcon } from './icons/ExportIcon'; import { XIcon } from './icons/XIcon'; +import apiService from '../services/apiService'; export const initialCampaigns = [ @@ -1110,10 +1111,27 @@ const ProofDetailView: React.FC<{ }; const handleDownload = async () => { - const url = selectedVersion.proofPreviewUrl; const fileName = `${proof.proofName}_V${selectedVersion.version}`; + const storageKey = selectedVersion.fileStorageKey; try { + // Prefer fetching the original file from backend storage + if (storageKey) { + const file = await apiService.getFile(storageKey); + const blobUrl = URL.createObjectURL(file); + const link = document.createElement('a'); + link.href = blobUrl; + const ext = storageKey.split('.').pop() || 'png'; + link.download = `${fileName}.${ext}`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(blobUrl); + return; + } + + // Fallback to preview URL if no storage key (legacy data) + const url = selectedVersion.proofPreviewUrl; if (url.startsWith('data:')) { // Data URL - extract mime type and download directly const link = document.createElement('a'); diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index 4318924..a87dd5b 100755 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -182,6 +182,17 @@ class ApiService { return this.fetch(`/proofs/${id}`); } + async getFile(storageKey: string): Promise { + const headers = await this.getHeaders(); + const response = await fetch(`${API_URL}/api/files/${encodeURIComponent(storageKey)}`, { headers }); + if (!response.ok) { + throw new Error(`Failed to fetch file: HTTP ${response.status}`); + } + const blob = await response.blob(); + const filename = storageKey.split('/').pop() || 'proof'; + return new File([blob], filename, { type: blob.type }); + } + async deleteProof(id: string): Promise { return this.fetch(`/proofs/${id}`, { method: 'DELETE', @@ -265,8 +276,10 @@ class ApiService { proofPreviewUrl: v.thumbnail_url || '', feedback: v.agent_review || {} as AgentReview, overallStatus: v.overall_status as any, + fileStorageKey: v.file_storage_key || '', })), _id: proof.id, + fileStorageKey: latestVersion?.file_storage_key || '', }; } @@ -361,6 +374,19 @@ class ApiService { async getAgencies(): Promise { return this.fetch('/agencies'); } + + // Support email endpoint + async sendSupportEmail(data: { + message: string; + subject: string; + user_name?: string; + user_email?: string; + }): Promise<{ success: boolean; message: string }> { + return this.fetch('/support/email', { + method: 'POST', + body: JSON.stringify(data), + }); + } } export interface DropdownOptionsResponse {