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:
parent
94a37f3ed8
commit
b119951f93
4 changed files with 115 additions and 4 deletions
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue