1- import { useState } from "react" ;
1+ import { useEffect , useRef , useState } from "react" ;
22import { useQuery , useQueryClient } from "@tanstack/react-query" ;
33import { formatId , useGlobalStore } from "../utils" ;
44import * as API from "aws-amplify/api" ;
@@ -39,6 +39,37 @@ export const QuickScans = () => {
3939 } ,
4040 } ) ;
4141
42+ const prevStatusesRef = useRef < Record < string , string > > ( { } ) ;
43+
44+ useEffect ( ( ) => {
45+ if ( ! quickScans ) return ;
46+ const prevStatuses = prevStatusesRef . current ;
47+ const nextStatuses : Record < string , string > = { } ;
48+ const justFinished : { url : string ; label : string } [ ] = [ ] ;
49+
50+ for ( const scan of quickScans ) {
51+ nextStatuses [ scan . id ] = scan . scan_status ;
52+ const wasActive =
53+ prevStatuses [ scan . id ] &&
54+ prevStatuses [ scan . id ] !== "complete" &&
55+ prevStatuses [ scan . id ] !== "failed" ;
56+ const isNowDone = scan . scan_status === "complete" || scan . scan_status === "failed" ;
57+ if ( wasActive && isNowDone ) {
58+ justFinished . push ( { url : scan . url , label : getScanStatusLabel ( scan ) } ) ;
59+ }
60+ }
61+ prevStatusesRef . current = nextStatuses ;
62+
63+ if ( justFinished . length === 1 ) {
64+ setAnnounceMessage (
65+ `Scan for ${ justFinished [ 0 ] . url } finished: ${ justFinished [ 0 ] . label } ` ,
66+ justFinished [ 0 ] . label === "Complete" ? "success" : "error"
67+ ) ;
68+ } else if ( justFinished . length > 1 ) {
69+ setAnnounceMessage ( `${ justFinished . length } scans finished` , "success" ) ;
70+ }
71+ } , [ quickScans , setAnnounceMessage ] ) ;
72+
4273 const formatUrl = ( input : string ) : string | null => {
4374 let u = input . trim ( ) ;
4475 if ( ! u ) return null ;
@@ -142,61 +173,66 @@ export const QuickScans = () => {
142173 < div className = { styles [ "quick-scans-list" ] } >
143174 < h2 > Scan History</ h2 >
144175 { isLoading ? (
145- < SkeletonAuditGrid count = { 3 } />
176+ < >
177+ < p className = "sr-only" role = "status" >
178+ Loading scan history…
179+ </ p >
180+ < SkeletonAuditGrid count = { 3 } />
181+ </ >
146182 ) : quickScans ?. length > 0 ? (
147- < div className = "cards-33" >
148- { quickScans . map ( ( scan : any , index : number ) => (
149- < Card variant = "light" key = { index } >
150- < Link to = { `/quick-scans/ ${ formatId ( scan . id ) } ` } >
151- < div className = { styles [ " scan-card" ] } >
152- < div >
153- < div className = { styles [ "scan-url" ] } > { scan . url } </ div >
154- < div className = { styles [ "scan-meta" ] } >
155- < span
156- className = { ` ${ styles [ "scan-status" ] } ${
157- styles [ scan . scan_status ] || ""
158- } ` }
159- >
160- { scan . scan_status === "processing"
161- ? `Scanning ${
162- scan . scan_percentage
163- ? ` ( ${ scan . scan_percentage } %)`
164- : "..."
165- } `
166- : scan . scan_status === "complete"
167- ? "Complete"
168- : scan . scan_status || "Pending" }
169- </ span >
170- < span > { scan . type ?. toUpperCase ( ) } </ span >
183+ < ul className = "cards-33" >
184+ { quickScans . map ( ( scan : any , index : number ) => {
185+ const statusLabel = getScanStatusLabel ( scan ) ;
186+ return (
187+ < li key = { scan . id ?? index } >
188+ < Card variant = "light" >
189+ < Link
190+ to = { `/quick-scans/ ${ formatId ( scan . id ) } ` }
191+ aria-label = { `View scan of ${ scan . url } , status: ${ statusLabel } , type: ${ scan . type ?. toUpperCase ( ) } ` }
192+ >
193+ < div className = { styles [ " scan-card" ] } >
194+ < div >
195+ < h3 className = { styles [ "scan-url" ] } > { scan . url } </ h3 >
196+ < div className = { styles [ "scan-meta" ] } >
197+ < span
198+ className = { ` ${ styles [ " scan-status" ] } ${
199+ styles [ scan . scan_status ] || ""
200+ } ` }
201+ >
202+ { statusLabel }
203+ </ span >
204+ < span > { scan . type ?. toUpperCase ( ) } </ span >
205+ </ div >
206+ </ div >
171207 </ div >
208+ </ Link >
209+ < div style = { { marginTop : "8px" } } >
210+ < DataRow
211+ variant = "highlight"
212+ the_key = "Blockers"
213+ the_value = { scan . blocker_count ?? "—" }
214+ />
215+ < DataRow
216+ the_key = "Scanned"
217+ the_value = {
218+ scan . scan_updated_at
219+ ? prettyDate ( scan . scan_updated_at ) +
220+ " at " +
221+ prettyTime ( scan . scan_updated_at )
222+ : "Not scanned yet"
223+ }
224+ />
225+ < DataRow
226+ variant = "no-border"
227+ the_key = "Created"
228+ the_value = { prettyDate ( scan . created_at ) }
229+ />
172230 </ div >
173- </ div >
174- </ Link >
175- < div style = { { marginTop : "8px" } } >
176- < DataRow
177- variant = "highlight"
178- the_key = "Blockers"
179- the_value = { scan . blocker_count ?? "—" }
180- />
181- < DataRow
182- the_key = "Scanned"
183- the_value = {
184- scan . scan_updated_at
185- ? prettyDate ( scan . scan_updated_at ) +
186- " at " +
187- prettyTime ( scan . scan_updated_at )
188- : "Not scanned yet"
189- }
190- />
191- < DataRow
192- variant = "no-border"
193- the_key = "Created"
194- the_value = { prettyDate ( scan . created_at ) }
195- />
196- </ div >
197- </ Card >
198- ) ) }
199- </ div >
231+ </ Card >
232+ </ li >
233+ ) ;
234+ } ) }
235+ </ ul >
200236 ) : (
201237 < Card variant = "light" >
202238 < div className = { styles [ "empty-state" ] } >
@@ -209,6 +245,14 @@ export const QuickScans = () => {
209245 ) ;
210246} ;
211247
248+ function getScanStatusLabel ( scan : any ) {
249+ if ( scan . scan_status === "processing" ) {
250+ return `Scanning${ scan . scan_percentage ? ` (${ scan . scan_percentage } %)` : "..." } ` ;
251+ }
252+ if ( scan . scan_status === "complete" ) return "Complete" ;
253+ return scan . scan_status || "Pending" ;
254+ }
255+
212256function prettyDate ( dateTime : string ) {
213257 return new Date ( dateTime ) . toLocaleDateString ( "en-US" , {
214258 weekday : "short" ,
0 commit comments