@@ -49,17 +49,24 @@ export default function App() {
4949 const fetchRadarData = async ( ) => {
5050 try {
5151 // ---- validate request ----
52+ //check if station is selected
5253 if ( ! selectedStation ?. properties ?. station_id ) {
5354 setErrorMessage ( "Please select a radar station first." ) ;
5455 return ;
5556 }
57+ // check if endTime is in the past
58+ if ( ! currentMode && selectedDateTime . isAfter ( dayjs ( ) . subtract ( Number ( selectedDuration ) , "minute" ) ) ) {
59+ setErrorMessage (
60+ `Please select a start time at least ${ selectedDuration } minutes in the past` ,
61+ ) ;
62+ return ;
63+ }
5664 setErrorMessage ( "" ) ;
5765 const durationMinutes = Number ( selectedDuration ) ;
5866 const requestBody = {
5967 stationId : selectedStation . properties . station_id ,
6068 } ;
6169 if ( ! currentMode ) {
62- console . log ( "using historical data" ) ;
6370 requestBody . startUtc = selectedDateTime
6471 . utc ( )
6572 . format ( "YYYY-MM-DDTHH:mm:ss[Z]" ) ;
@@ -68,7 +75,6 @@ export default function App() {
6875 . utc ( )
6976 . format ( "YYYY-MM-DDTHH:mm:ss[Z]" ) ;
7077 } else {
71- console . log ( "using current data" ) ;
7278 requestBody . startUtc = dayjs ( )
7379 . subtract ( durationMinutes + 15 , "minute" )
7480 . utc ( )
@@ -119,23 +125,31 @@ export default function App() {
119125 } ;
120126
121127 // fetch frames once the job is completed and the jobId and numFrames are set
122- useEffect ( ( ) => {
123- async function fetchFrames ( ) {
124- if ( jobStatus !== "COMPLETED" || ! jobId || numFrames <= 0 ) return ;
125- console . log ( `attempting to fetch ${ numFrames } frames for job ${ jobId } ` ) ;
126- try {
127- const promises = Array . from ( { length : numFrames } , ( _ , i ) =>
128- fetch ( `/apis/jobs/${ jobId } /frames/${ i } ` )
129- . then ( ( res ) => {
130- if ( ! res . ok ) throw new Error ( `Failed frame ${ i } ` ) ;
131- return res . blob ( ) ;
132- } )
133- . then ( ( blob ) => URL . createObjectURL ( blob ) ) ,
134- ) ;
135- const urls = await Promise . all ( promises ) ;
136- setFrames ( urls ) ;
137- setIsPlaying ( true ) ;
138- console . log ( "Frames fetched successfully: " , urls ) ;
128+ useEffect ( ( ) => {
129+ async function fetchFrames ( ) {
130+ if ( jobStatus !== "COMPLETED" || ! jobId || numFrames <= 0 ) return ;
131+ console . log ( `attempting to fetch ${ numFrames } frames for job ${ jobId } ` ) ;
132+ try {
133+ const promises = Array . from ( { length : numFrames } , async ( _ , i ) => {
134+ const res = await fetch ( `/apis/jobs/${ jobId } /frames/${ i } ` ) ;
135+ if ( res . status === 404 ) {
136+ console . warn ( `Frame ${ i } gave 404 - skipping` ) ;
137+ return null ;
138+ }
139+ if ( ! res . ok ) throw new Error ( `Failed frame ${ i } ` ) ;
140+ const timestamp = res . headers . get ( "x-frame-timestamp" ) ;
141+ const blob = await res . blob ( ) ;
142+ return {
143+ url : URL . createObjectURL ( blob ) ,
144+ timestamp,
145+ index : i ,
146+ } ;
147+ } ) ;
148+ const frames = await Promise . all ( promises ) ;
149+ frames . filter ( Boolean ) . sort ( ( a , b ) => a . index - b . index ) ;
150+ setFrames ( frames ) ;
151+ setIsPlaying ( frames . length > 0 ) ;
152+ console . log ( "Frames fetched successfully: " , frames ) ;
139153 } catch ( err ) {
140154 console . error ( "Error fetching frames:" , err ) ;
141155 }
@@ -219,7 +233,10 @@ export default function App() {
219233 return (
220234 < div >
221235 < div className = "flex flex-col md:flex-row w-full" >
222- < div className = "md:mt-12 p-4 gap-4 md:w-92 w-full flex flex-col" >
236+ < div className = "md:mt-6 p-4 gap-4 md:w-92 w-full flex flex-col" >
237+ < div className = "mb-6 items-center gap-4" >
238+ < h1 className = "text-3xl font-light" > Gust Front Web App</ h1 >
239+ </ div >
223240 { /* Station Selector */ }
224241 < RadarStationDropdown
225242 stations = { stations }
@@ -360,11 +377,10 @@ export default function App() {
360377 < p className = "min-w-fit px-3" > { geotiffOpacity } %</ p >
361378 </ div >
362379 < div className = "w-1/2" >
363- { /*TODO: Actual timestamp will go here: */ }
364380 < p className = "text-sm text-right" >
365- { selectedDateTime
366- . tz ( timezone )
367- . format ( "YYYY-MM-DD HH:mm z" ) }
381+ { frames [ currentFrameIndex ] ?. timestamp
382+ ? ` ${ dayjs ( frames [ currentFrameIndex ] . timestamp ) . tz ( timezone ) . format ( "YYYY-MM-DD HH:mm z" ) } `
383+ : "No timestamp available" }
368384 </ p >
369385 </ div >
370386 </ div >
@@ -378,8 +394,8 @@ export default function App() {
378394 < div
379395 className = {
380396 jobStatus === "PROCESSING" ||
381- jobStatus === "REQUESTED" ||
382- jobStatus === "PENDING"
397+ jobStatus === "REQUESTED" ||
398+ jobStatus === "PENDING"
383399 ? "opacity-50"
384400 : ""
385401 }
@@ -395,6 +411,12 @@ export default function App() {
395411 </ div >
396412 </ div >
397413 </ div >
414+ < footer className = " m-4 absolute bottom-0 left-0 hidden md:block shadow-xl hover:shadow-sm transition-all" >
415+ < a className = "outline-1 hover:text-black opacity-50 hover:opacity-100 transition-all rounded-md p-2 flex gap-2 items-center" href = "https://github.com/firelab/gust-front-detection-webapp" target = "_blank" rel = "noopener noreferrer" >
416+ < p className = "" > Code</ p >
417+ < img src = "/assets/github.svg" alt = "GitHub" className = "w-6 h-6" />
418+ </ a >
419+ </ footer >
398420 </ div >
399421 ) ;
400422}
0 commit comments