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 <noreply@anthropic.com>
This commit is contained in:
michael 2026-01-24 07:01:58 -06:00
parent 94a37f3ed8
commit b119951f93
4 changed files with 115 additions and 4 deletions

View file

@ -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"}

View file

@ -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,

View file

@ -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');

View file

@ -182,6 +182,17 @@ class ApiService {
return this.fetch<ProofResponse>(`/proofs/${id}`);
}
async getFile(storageKey: string): Promise<File> {
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<void> {
return this.fetch<void>(`/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<AgencyResponse[]> {
return this.fetch<AgencyResponse[]>('/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 {