diff --git a/backend/apis/run_request.py b/backend/apis/run_request.py index 4b3218d..de189fb 100644 --- a/backend/apis/run_request.py +++ b/backend/apis/run_request.py @@ -86,10 +86,6 @@ def validate_time_parameters(request_fields: dict): except ValueError: return jsonify({"error": "Invalid datetime format. Expected ISO 8601: YYYY-MM-DDTHH:MM:SSZ"}) - # startUtc must be within the last 2 hours - if start_utc < now - timedelta(minutes=120): - return jsonify({"error": "startUtc must be within the last 2 hours"}) - # endUtc must be after startUtc if end_utc <= start_utc: return jsonify({"error": "endUtc must be after startUtc"}) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 154c0cc..62fff5d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,6 +24,7 @@ "tailwindcss": "^4.1.18" }, "devDependencies": { + "@biomejs/biome": "^2.4.10", "@eslint/js": "^9.39.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", @@ -281,6 +282,169 @@ "node": ">=6.9.0" } }, + "node_modules/@biomejs/biome": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.10.tgz", + "integrity": "sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.10", + "@biomejs/cli-darwin-x64": "2.4.10", + "@biomejs/cli-linux-arm64": "2.4.10", + "@biomejs/cli-linux-arm64-musl": "2.4.10", + "@biomejs/cli-linux-x64": "2.4.10", + "@biomejs/cli-linux-x64-musl": "2.4.10", + "@biomejs/cli-win32-arm64": "2.4.10", + "@biomejs/cli-win32-x64": "2.4.10" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.10.tgz", + "integrity": "sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.10.tgz", + "integrity": "sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.10.tgz", + "integrity": "sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.10.tgz", + "integrity": "sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.10.tgz", + "integrity": "sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.10.tgz", + "integrity": "sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.10.tgz", + "integrity": "sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.10.tgz", + "integrity": "sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6cba051..bb1a018 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ "tailwindcss": "^4.1.18" }, "devDependencies": { + "@biomejs/biome": "^2.4.10", "@eslint/js": "^9.39.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e06c83e..ea04ad8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,28 +1,37 @@ -import Container from '@mui/material/Container'; -import 'leaflet/dist/leaflet.css'; -import LeafletMap from './components/LeafletMap' -import RadarStationDropdown from "./components/RadarStationDropdown"; -import { useState, useEffect, useRef } from 'react'; -import dayjs from './utils/dayjsConfig'; - +import Container from "@mui/material/Container"; +import "leaflet/dist/leaflet.css"; +import PauseIcon from "@mui/icons-material/Pause"; +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; +import { + Button, + Checkbox, + FormControl, + InputLabel, + MenuItem, + Select, + Slider, +} from "@mui/material"; // MUI -import { LocalizationProvider, DateTimePicker } from '@mui/x-date-pickers'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import PlayArrowIcon from '@mui/icons-material/PlayArrow'; -import PauseIcon from '@mui/icons-material/Pause'; -import { Slider, Button, Select, MenuItem, FormControl, InputLabel, Checkbox } from '@mui/material'; +import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { useEffect, useRef, useState } from "react"; +import LeafletMap from "./components/LeafletMap"; +import RadarStationDropdown from "./components/RadarStationDropdown"; +import dayjs from "./utils/dayjsConfig"; export default function App() { - // User Selection State const [stations, setStations] = useState([]); const [currentMode, setCurrentMode] = useState(true); const [selectedStation, setSelectedStation] = useState(""); - const [selectedDateTime, setSelectedDateTime] = useState(dayjs().tz(dayjs.tz.guess())); + const [selectedDateTime, setSelectedDateTime] = useState( + dayjs().tz(dayjs.tz.guess()), + ); const [timezone, setTimezone] = useState(dayjs.tz.guess()); + const [selectedDuration, setSelectedDuration] = useState("60"); // API State - const [jobStatus, setjobStatus] = useState("NONE"); + const [jobStatus, setJobStatus] = useState("NONE"); const [jobId, setjobId] = useState(""); const [numFrames, setNumFrames] = useState(0); const [frames, setFrames] = useState([]); @@ -32,7 +41,7 @@ export default function App() { const [currentFrameIndex, setCurrentFrameIndex] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const playbackRef = useRef(null); - + // --------------------------------------- HANDLERS ---------------------------------------- // requests a job from /backend/apis/run_request.py and recieves a job_id and response code @@ -40,70 +49,97 @@ export default function App() { try { // ---- validate request ---- if (!selectedStation?.properties?.station_id) { - throw new Error("No station selected"); + setErrorMessage("Please select a radar station first."); + return; } + setErrorMessage(""); + const durationMinutes = Number(selectedDuration); const requestBody = { - stationId: selectedStation.properties.station_id + stationId: selectedStation.properties.station_id, }; - if (!currentMode){ + if (!currentMode) { console.log("using historical data"); - // if not fetching current data the end time requested is t+30 minutes - requestBody.startUtc = selectedDateTime.utc().format("YYYY-MM-DDTHH:mm:ss[Z]") - requestBody.endUtc = selectedDateTime.add(30, 'minute').utc().format("YYYY-MM-DDTHH:mm:ss[Z]") + requestBody.startUtc = selectedDateTime + .utc() + .format("YYYY-MM-DDTHH:mm:ss[Z]"); + requestBody.endUtc = selectedDateTime + .add(durationMinutes, "minute") + .utc() + .format("YYYY-MM-DDTHH:mm:ss[Z]"); } else { - // currently using the same timebox as the default in run_request.py when no timebox is provided console.log("using current data"); - requestBody.startUtc = dayjs().subtract(45, 'minute').utc().format("YYYY-MM-DDTHH:mm:ss[Z]") - requestBody.endUtc = dayjs().subtract(25, 'minute').utc().format("YYYY-MM-DDTHH:mm:ss[Z]") + requestBody.startUtc = dayjs() + .subtract(durationMinutes + 15, "minute") + .utc() + .format("YYYY-MM-DDTHH:mm:ss[Z]"); + requestBody.endUtc = dayjs() + .subtract(15, "minute") + .utc() + .format("YYYY-MM-DDTHH:mm:ss[Z]"); } - // ---- make request ---- - setjobStatus("REQUESTED"); - setErrorMessage(""); + + // ---- reset state ---- + setJobStatus("REQUESTED"); setjobId(""); setNumFrames(0); setFrames([]); + + // ---- make request ---- const response = await fetch("/apis/run", { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' + "Content-Type": "application/json", + Accept: "application/json", }, - body: JSON.stringify(requestBody) + body: JSON.stringify(requestBody), }); + + // ---- Handle Errors ---- if (!response.ok) { - throw new Error(`Request failed with status ${response.status}`); + const errorData = await response.json().catch(() => ({})); + const errorMsg = + errorData.error || + errorData.message || + `Error ${response.status}: ${response.statusText}`; + setErrorMessage(errorMsg); + setJobStatus("FAILED"); + throw new Error(errorMsg); // This sends the message to the catch block } + const data = await response.json(); - console.log(data) setjobId(data.job_id); } catch (err) { - console.error(err) + console.error("Fetch Error:", err); + setJobStatus("FAILED"); + if (!errorMessage) { + setErrorMessage("A network error occurred. Please try again."); + } } }; // fetch frames once the job is completed and the jobId and numFrames are set -useEffect(() => { - async function fetchFrames() { - if (jobStatus !== "COMPLETED" || !jobId || numFrames <= 0) return; - console.log(`attempting to fetch ${numFrames} frames for job ${jobId}`); - try { - const promises = Array.from({ length: numFrames }, (_, i) => - fetch(`/apis/jobs/${jobId}/frames/${i}`) - .then(res => { - if (!res.ok) throw new Error(`Failed frame ${i}`); - return res.blob(); - }) - .then(blob => URL.createObjectURL(blob)) - ); - const urls = await Promise.all(promises); - setFrames(urls); - console.log("Frames fetched successfully: ", urls); - } catch (err) { - console.error("Error fetching frames:", err); + useEffect(() => { + async function fetchFrames() { + if (jobStatus !== "COMPLETED" || !jobId || numFrames <= 0) return; + console.log(`attempting to fetch ${numFrames} frames for job ${jobId}`); + try { + const promises = Array.from({ length: numFrames }, (_, i) => + fetch(`/apis/jobs/${jobId}/frames/${i}`) + .then((res) => { + if (!res.ok) throw new Error(`Failed frame ${i}`); + return res.blob(); + }) + .then((blob) => URL.createObjectURL(blob)), + ); + const urls = await Promise.all(promises); + setFrames(urls); + console.log("Frames fetched successfully: ", urls); + } catch (err) { + console.error("Error fetching frames:", err); + } } - } - fetchFrames(); -}, [jobStatus, jobId, numFrames]); + fetchFrames(); + }, [jobStatus, jobId, numFrames]); // fetch radar stations from backend at /apis/stations useEffect(() => { @@ -129,30 +165,31 @@ useEffect(() => { const response = await fetch(`/apis/status?job_id=${jobId}`); const data = await response.json(); console.log(data); - setjobStatus(data.status); - if (data.error_message){ + setJobStatus(data.status); + if (data.error) { setErrorMessage(data.error_message); + console.log("here"); } else { setErrorMessage(""); } - if (data.num_frames){ + if (data.num_frames) { setNumFrames(data.num_frames); } else { setNumFrames(0); } - } catch (err) { - console.error(err); - } + } catch (err) { + console.error(err); + } }, 5000); return () => clearInterval(intervalId); }, [jobId, jobStatus]); // timezone change handler function handleTimezoneChange(event) { - const newTZ = event.target.value - setTimezone(newTZ) + const newTZ = event.target.value; + setTimezone(newTZ); if (selectedDateTime) { - setSelectedDateTime(selectedDateTime.tz(newTZ)) + setSelectedDateTime(selectedDateTime.tz(newTZ)); } } @@ -161,7 +198,7 @@ useEffect(() => { if (isPlaying) { playbackRef.current = setInterval(() => { setCurrentFrameIndex((prev) => (prev + 1) % frames.length); - }, 750); + }, 300); } else { clearInterval(playbackRef.current); } @@ -178,31 +215,54 @@ useEffect(() => { // ---------------------------------------- JSX ---------------------------------------- return ( - -
+
+
+
{/* Station Selector */} -
- -
-
+ +
- {setCurrentMode(!currentMode)}}> - -

Use Current Data

+ { + setCurrentMode(!currentMode); + }} + > +
- {/* Timezone Selector */} + + Duration + + + {/* Timezone Selector */} Timezone ); -} \ No newline at end of file +}