diff --git a/apps/frontend/Dockerfile b/apps/frontend/Dockerfile index a5be6be6..fe33f704 100644 --- a/apps/frontend/Dockerfile +++ b/apps/frontend/Dockerfile @@ -1,5 +1,5 @@ # 1. Install dependencies only when needed -FROM node:18-alpine AS deps +FROM node:20-alpine AS deps WORKDIR /app @@ -35,6 +35,7 @@ COPY --from=builder /app/public ./public COPY --from=builder /app/.next ./.next COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/next.config.ts ./ # Expose the port Next.js runs on EXPOSE 3000 diff --git a/apps/frontend/next.config.ts b/apps/frontend/next.config.ts index e9ffa308..a726b9e3 100644 --- a/apps/frontend/next.config.ts +++ b/apps/frontend/next.config.ts @@ -2,6 +2,20 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ + experimental: { + serverActions: { + bodySizeLimit: '100mb', + }, + }, + + async rewrites() { + return [ + { + source: '/api/:path*', + destination: 'http://backend:8000/api/:path*' // Routes traffic safely inside the Docker network + } + ]; + } }; export default nextConfig; diff --git a/apps/frontend/src/app/_home/UploadButtons.tsx b/apps/frontend/src/app/_home/UploadButtons.tsx index 5c141393..466e700f 100644 --- a/apps/frontend/src/app/_home/UploadButtons.tsx +++ b/apps/frontend/src/app/_home/UploadButtons.tsx @@ -6,11 +6,8 @@ import { cn } from "@/lib/utils"; import { UploadDataType, UploadModalityType } from "@/enums"; import { Upload as UploadIcon } from "lucide-react"; import { useAppContext } from "@/providers/AppProvider"; -import { findNiftiFile, findRelevantFiles } from "@/utils"; -import { getReport } from "@/services/apiReport"; import { toast } from "sonner"; import { useRouter } from "next/navigation"; -import { IAllRelevantFilesType } from "@/types"; import { handleBidsUpload, handleDicomUpload } from "./uploadHandlers"; type TUploadDataOptions = (typeof UploadDataType)[keyof typeof UploadDataType]; @@ -62,7 +59,7 @@ const UploadButtons = () => { setApiData(data); if ( data.missing_required_parameters && - data.missing_required_parameters.length > 0 + Number(data.missing_required_parameters.length) > 0 ) { toast.info( "Report generated with missing parameters. Please provide the missing values." diff --git a/apps/frontend/src/app/_home/uploadHandlers.ts b/apps/frontend/src/app/_home/uploadHandlers.ts index 32008058..6ff91c2c 100644 --- a/apps/frontend/src/app/_home/uploadHandlers.ts +++ b/apps/frontend/src/app/_home/uploadHandlers.ts @@ -13,8 +13,8 @@ const handleDicomUpload = async ({ setIsLoading: (v: boolean) => void; activeModalityTypeOption: UploadModalityType; }) => { - setIsLoading(true); - try { + if (!files || files.length === 0) return; + const filesArray = Array.from(files); // DICOM files: .dcm, .img, or no extension @@ -25,29 +25,28 @@ const handleDicomUpload = async ({ !file.name.includes(".") ); - const formData = new FormData(); - - dicomFiles.forEach((file) => { - formData.append("dcm_files", file); - }); - - formData.append("modality", activeModalityTypeOption); - - const data = await getReport(formData, UploadDataType.DICOM.toLowerCase()); - - setIsLoading(false); - - return data; + // PRE-FLIGHT VALIDATION: Fail fast if no valid files exist + if (dicomFiles.length === 0) { + toast.error("No valid DICOM files found in the selected folder. Please ensure the folder contains .dcm or .img files."); + return; + } - } catch (error) { + setIsLoading(true); + try { + const formData = new FormData(); + dicomFiles.forEach((file) => formData.append("dcm_files", file)); + formData.append("modality", activeModalityTypeOption); - setIsLoading(false); - toast.error( - `An unexpected error occurred during upload, please try again. Error: ${error}` - ); + const data = await getReport(formData, UploadDataType.DICOM.toLowerCase()); + setIsLoading(false); + return data; + } catch (error) { + console.error("Upload error:", error); + setIsLoading(false); + toast.error(`Upload failed. Please check your network connection and try again.`); + } } -} const handleBidsUpload = async ({ files, @@ -61,70 +60,64 @@ const handleBidsUpload = async ({ files: FileList; setIsLoading: (v: boolean) => void; setUploadedFiles: (files: IAllRelevantFilesType) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any setUploadConfig: (config: any) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any setUpdatedJsonContent: (content: any) => void; setUpdatedJsonFilename: (filename: string) => void; activeModalityTypeOption: UploadModalityType; }) => { - setIsLoading(true); + if (!files || files.length === 0) return; - try { const filesArray = Array.from(files); const niftiFile = findNiftiFile(filesArray); const aslRelevantFiles = findRelevantFiles(filesArray); - const allRelevantFiles: IAllRelevantFilesType = { - nifti_file: niftiFile, - asl_files: aslRelevantFiles, - dicom_files: [], - }; - - const formData = new FormData(); - - if (niftiFile) { - formData.append("nifti_file", niftiFile); + // PRE-FLIGHT VALIDATION: Prevent empty backend requests + if (!niftiFile && aslRelevantFiles.length === 0) { + toast.error("No valid BIDS format files found. Please check your dataset."); + return; } - filesArray.forEach((file) => { - if (file.name.endsWith(".dcm")) { - formData.append("dcm_files", file); - allRelevantFiles.dicom_files.push(file); - } - }); - - aslRelevantFiles.forEach((file) => { - formData.append("files", file); - formData.append("filenames", file.name); - }); - - setUploadedFiles(allRelevantFiles); - - setUploadConfig({ - modalityType: activeModalityTypeOption, - fileType: UploadDataType.BIDS, - }); - - setUpdatedJsonContent(null); - setUpdatedJsonFilename(''); - - formData.append("modality", activeModalityTypeOption); - - const data = await getReport(formData, UploadDataType.BIDS.toLowerCase()); - - setIsLoading(false); - - return data; - - } catch (error) { - setIsLoading(false); - toast.error( - `An unexpected error occurred during upload, please try again. Error: ${error}` - ); - } + setIsLoading(true); + try { + const allRelevantFiles: IAllRelevantFilesType = { + nifti_file: niftiFile, + asl_files: aslRelevantFiles, + dicom_files: [], + }; + + const formData = new FormData(); + if (niftiFile) formData.append("nifti_file", niftiFile); + + filesArray.forEach((file) => { + if (file.name.endsWith(".dcm")) { + formData.append("dcm_files", file); + allRelevantFiles.dicom_files.push(file); + } + }); + + aslRelevantFiles.forEach((file) => { + formData.append("files", file); + formData.append("filenames", file.name); + }); + + setUploadedFiles(allRelevantFiles); + setUploadConfig({ modalityType: activeModalityTypeOption, fileType: UploadDataType.BIDS }); + setUpdatedJsonContent(null); + setUpdatedJsonFilename(''); + formData.append("modality", activeModalityTypeOption); + + const data = await getReport(formData, UploadDataType.BIDS.toLowerCase()); + setIsLoading(false); + return data; + + } catch (error) { + console.error("Upload error:", error); + setIsLoading(false); + toast.error(`Upload failed. Please check your network connection and try again.`); + } } -export { - handleDicomUpload, - handleBidsUpload -} \ No newline at end of file +export { handleDicomUpload, handleBidsUpload } \ No newline at end of file diff --git a/apps/frontend/src/services/apiReport.ts b/apps/frontend/src/services/apiReport.ts index 1f74606c..4a7d95ea 100644 --- a/apps/frontend/src/services/apiReport.ts +++ b/apps/frontend/src/services/apiReport.ts @@ -2,7 +2,7 @@ import axios from 'axios'; import {IReportApiResponse} from '@/types'; -const API_BASE_URL = `${process.env.NEXT_PUBLIC_API_BASE_URL}/report`; +const API_BASE_URL = `/api/report`; const client = axios.create({ baseURL: API_BASE_URL,