11"use client" ;
22
3- import { useRef , useState } from "react" ;
3+ import { useMemo , useRef , useState } from "react" ;
44
55export default function Home ( ) {
66 const fileRef = useRef < HTMLInputElement > ( null ) ;
77 const [ selectedFile , setSelectedFile ] = useState < File | null > ( null ) ;
88 const [ status , setStatus ] = useState < string | null > ( null ) ;
9- const [ result , setResult ] = useState < string | null > ( null ) ;
9+ const [ result , setResult ] = useState < Record < string , unknown > | null > ( null ) ;
10+ const [ rawResult , setRawResult ] = useState < string | null > ( null ) ;
1011 const [ isUploading , setIsUploading ] = useState ( false ) ;
1112
1213 const handleChooseFile = ( ) => {
@@ -18,6 +19,7 @@ export default function Home() {
1819 setSelectedFile ( file ) ;
1920 setStatus ( file ? `Selected ${ file . name } ` : "No file selected" ) ;
2021 setResult ( null ) ;
22+ setRawResult ( null ) ;
2123 } ;
2224
2325 const handleUpload = async ( ) => {
@@ -55,24 +57,53 @@ export default function Home() {
5557 ? String ( ( payload as { error ?: string } ) . error )
5658 : `Scan failed (${ response . status } )` ;
5759 setStatus ( message ) ;
58- setResult ( rawText || null ) ;
60+ setResult ( null ) ;
61+ setRawResult ( rawText || null ) ;
5962 return ;
6063 }
6164
6265 setStatus ( "Scan complete" ) ;
6366 if ( payload && typeof payload === "object" ) {
64- setResult ( JSON . stringify ( payload , null , 2 ) ) ;
67+ setResult ( payload as Record < string , unknown > ) ;
68+ setRawResult ( JSON . stringify ( payload , null , 2 ) ) ;
6569 } else {
66- setResult ( rawText || null ) ;
70+ setResult ( null ) ;
71+ setRawResult ( rawText || null ) ;
6772 }
6873 } catch ( error ) {
6974 setStatus ( "Failed to reach backend. Is it running on :7070?" ) ;
7075 setResult ( null ) ;
76+ setRawResult ( null ) ;
7177 } finally {
7278 setIsUploading ( false ) ;
7379 }
7480 } ;
7581
82+ const summary = useMemo ( ( ) => {
83+ const report = result ?. report as
84+ | { results ?: Array < Record < string , unknown > > ; total_duration_ms ?: number }
85+ | undefined ;
86+ const results = report ?. results ?? [ ] ;
87+ const failures = results . filter ( ( item ) => {
88+ const status = item . status as string | undefined ;
89+ return status === "Fail" || status === "Error" ;
90+ } ) ;
91+ const errorCount = failures . filter ( ( item ) => item . severity === "Error" ) . length ;
92+ const warningCount = failures . filter ( ( item ) => item . severity === "Warning" ) . length ;
93+ const duration =
94+ typeof report ?. total_duration_ms === "number"
95+ ? `${ report . total_duration_ms } ms`
96+ : null ;
97+
98+ return {
99+ results,
100+ failures,
101+ errorCount,
102+ warningCount,
103+ duration,
104+ } ;
105+ } , [ result ] ) ;
106+
76107 return (
77108 < div className = "page" >
78109 < div className = "page-glow page-glow--left" />
@@ -177,10 +208,81 @@ export default function Home() {
177208 </ div >
178209 { status ? < div className = "status-pill" > { status } </ div > : null }
179210 { result ? (
180- < details className = "result-card" open >
181- < summary className = "result-header" > Latest report</ summary >
182- < pre > { result } </ pre >
183- </ details >
211+ < div className = "report-stack" >
212+ < div className = "report-summary" >
213+ < div >
214+ < div className = "summary-label" > Errors</ div >
215+ < div className = "summary-value" > { summary . errorCount } </ div >
216+ </ div >
217+ < div >
218+ < div className = "summary-label" > Warnings</ div >
219+ < div className = "summary-value" > { summary . warningCount } </ div >
220+ </ div >
221+ < div >
222+ < div className = "summary-label" > Findings</ div >
223+ < div className = "summary-value" > { summary . failures . length } </ div >
224+ </ div >
225+ < div >
226+ < div className = "summary-label" > Duration</ div >
227+ < div className = "summary-value" > { summary . duration ?? "—" } </ div >
228+ </ div >
229+ </ div >
230+
231+ < div className = "result-card" >
232+ < div className = "result-header" > Top findings</ div >
233+ < ul className = "finding-list" >
234+ { summary . failures . slice ( 0 , 5 ) . map ( ( item , index ) => (
235+ < li key = { `${ item . rule_id ?? "rule" } -${ index } ` } >
236+ < strong > { String ( item . rule_name ?? "Untitled rule" ) } </ strong >
237+ < span > { String ( item . recommendation ?? "Review this rule" ) } </ span >
238+ </ li >
239+ ) ) }
240+ { summary . failures . length === 0 ? (
241+ < li className = "finding-empty" > No failing rules detected.</ li >
242+ ) : null }
243+ </ ul >
244+ </ div >
245+
246+ < div className = "result-card" >
247+ < div className = "result-header" > Report actions</ div >
248+ < div className = "report-actions" >
249+ < button
250+ className = "secondary-button"
251+ type = "button"
252+ onClick = { ( ) => {
253+ if ( ! rawResult ) return ;
254+ const blob = new Blob ( [ rawResult ] , { type : "application/json" } ) ;
255+ const url = URL . createObjectURL ( blob ) ;
256+ const link = document . createElement ( "a" ) ;
257+ link . href = url ;
258+ link . download = "verifyos-report.json" ;
259+ link . click ( ) ;
260+ URL . revokeObjectURL ( url ) ;
261+ } }
262+ >
263+ Download JSON
264+ </ button >
265+ < button
266+ className = "ghost-button"
267+ type = "button"
268+ onClick = { ( ) => {
269+ if ( ! rawResult ) return ;
270+ void navigator . clipboard ?. writeText ( rawResult ) ;
271+ setStatus ( "Report copied to clipboard" ) ;
272+ } }
273+ >
274+ Copy JSON
275+ </ button >
276+ </ div >
277+ </ div >
278+
279+ { rawResult ? (
280+ < details className = "result-card" >
281+ < summary className = "result-header" > Raw report</ summary >
282+ < pre > { rawResult } </ pre >
283+ </ details >
284+ ) : null }
285+ </ div >
184286 ) : null }
185287 < div className = "card-footer" >
186288 < div >
0 commit comments