@@ -4,11 +4,15 @@ import { useMemo, useRef, useState } from "react";
44
55export default function Home ( ) {
66 const fileRef = useRef < HTMLInputElement > ( null ) ;
7+ const projectRef = useRef < HTMLInputElement > ( null ) ;
78 const [ selectedFile , setSelectedFile ] = useState < File | null > ( null ) ;
9+ const [ projectFile , setProjectFile ] = useState < File | null > ( null ) ;
810 const [ status , setStatus ] = useState < string | null > ( null ) ;
911 const [ result , setResult ] = useState < Record < string , unknown > | null > ( null ) ;
1012 const [ rawResult , setRawResult ] = useState < string | null > ( null ) ;
1113 const [ isUploading , setIsUploading ] = useState ( false ) ;
14+ const [ profile , setProfile ] = useState < "basic" | "full" > ( "full" ) ;
15+ const [ showAdvanced , setShowAdvanced ] = useState ( false ) ;
1216
1317 const examplePayload = {
1418 report : {
@@ -67,6 +71,12 @@ export default function Home() {
6771 setRawResult ( null ) ;
6872 } ;
6973
74+ const handleProjectChange = ( event : React . ChangeEvent < HTMLInputElement > ) => {
75+ const file = event . target . files ?. [ 0 ] ?? null ;
76+ setProjectFile ( file ) ;
77+ setStatus ( file ? `Attached ${ file . name } ` : "Project removed" ) ;
78+ } ;
79+
7080 const handleUpload = async ( ) => {
7181 if ( ! selectedFile || isUploading ) {
7282 return ;
@@ -79,7 +89,10 @@ export default function Home() {
7989 try {
8090 const form = new FormData ( ) ;
8191 form . append ( "bundle" , selectedFile ) ;
82- form . append ( "profile" , "full" ) ;
92+ form . append ( "profile" , profile ) ;
93+ if ( projectFile ) {
94+ form . append ( "project" , projectFile ) ;
95+ }
8396
8497 const response = await fetch ( "http://127.0.0.1:7070/api/v1/scan" , {
8598 method : "POST" ,
@@ -133,7 +146,10 @@ export default function Home() {
133146
134147 const summary = useMemo ( ( ) => {
135148 const report = result ?. report as
136- | { results ?: Array < Record < string , unknown > > ; total_duration_ms ?: number }
149+ | {
150+ results ?: Array < Record < string , unknown > > ;
151+ total_duration_ms ?: number ;
152+ }
137153 | undefined ;
138154 const results = report ?. results ?? [ ] ;
139155 const failures = results . filter ( ( item ) => {
@@ -147,12 +163,26 @@ export default function Home() {
147163 ? `${ report . total_duration_ms } ms`
148164 : null ;
149165
166+ const byCategory = results . reduce < Record < string , number > > ( ( acc , item ) => {
167+ const category = String ( item . category ?? "Other" ) ;
168+ acc [ category ] = ( acc [ category ] ?? 0 ) + 1 ;
169+ return acc ;
170+ } , { } ) ;
171+
172+ const bySeverity = results . reduce < Record < string , number > > ( ( acc , item ) => {
173+ const severity = String ( item . severity ?? "Unknown" ) ;
174+ acc [ severity ] = ( acc [ severity ] ?? 0 ) + 1 ;
175+ return acc ;
176+ } , { } ) ;
177+
150178 return {
151179 results,
152180 failures,
153181 errorCount,
154182 warningCount,
155183 duration,
184+ byCategory,
185+ bySeverity,
156186 } ;
157187 } , [ result ] ) ;
158188
@@ -291,6 +321,39 @@ export default function Home() {
291321 { selectedFile ? selectedFile . name : "No file selected" }
292322 </ div >
293323 </ div >
324+ { showAdvanced ? (
325+ < div className = "advanced-panel" >
326+ < div className = "advanced-row" >
327+ < label htmlFor = "profile-select" > Profile</ label >
328+ < select
329+ id = "profile-select"
330+ value = { profile }
331+ onChange = { ( event ) =>
332+ setProfile ( event . target . value as "basic" | "full" )
333+ }
334+ >
335+ < option value = "basic" > Basic (core rules)</ option >
336+ < option value = "full" > Full (all rules)</ option >
337+ </ select >
338+ </ div >
339+ < div className = "advanced-row" >
340+ < label htmlFor = "project-file" > Project zip (optional)</ label >
341+ < input
342+ ref = { projectRef }
343+ id = "project-file"
344+ type = "file"
345+ accept = ".zip"
346+ onChange = { handleProjectChange }
347+ />
348+ < span className = "advanced-hint" >
349+ Zip your .xcodeproj or .xcworkspace to include project context.
350+ </ span >
351+ </ div >
352+ { projectFile ? (
353+ < div className = "upload-status" > Attached { projectFile . name } </ div >
354+ ) : null }
355+ </ div >
356+ ) : null }
294357 { status ? < div className = "status-pill" > { status } </ div > : null }
295358 { result ? (
296359 < div className = "report-stack" >
@@ -328,6 +391,36 @@ export default function Home() {
328391 </ ul >
329392 </ div >
330393
394+ < div className = "result-card" >
395+ < div className = "result-header" > Findings by category</ div >
396+ < div className = "bar-list" >
397+ { Object . entries ( summary . byCategory ) . map ( ( [ name , count ] ) => (
398+ < div key = { name } className = "bar-row" >
399+ < span > { name } </ span >
400+ < div className = "bar" >
401+ < div
402+ className = "bar-fill"
403+ style = { { width : `${ Math . min ( count * 12 , 100 ) } %` } }
404+ />
405+ </ div >
406+ < strong > { count } </ strong >
407+ </ div >
408+ ) ) }
409+ </ div >
410+ </ div >
411+
412+ < div className = "result-card" >
413+ < div className = "result-header" > Findings by severity</ div >
414+ < div className = "pill-row" >
415+ { Object . entries ( summary . bySeverity ) . map ( ( [ name , count ] ) => (
416+ < div key = { name } className = "pill-chip" >
417+ < span > { name } </ span >
418+ < strong > { count } </ strong >
419+ </ div >
420+ ) ) }
421+ </ div >
422+ </ div >
423+
331424 < div className = "result-card" >
332425 < div className = "result-header" > Report actions</ div >
333426 < div className = "report-actions" >
@@ -373,8 +466,12 @@ export default function Home() {
373466 < div >
374467 < strong > Next:</ strong > privacy manifest, entitlements, ATS rules
375468 </ div >
376- < button className = "ghost-button" type = "button" >
377- Advanced options
469+ < button
470+ className = "ghost-button"
471+ type = "button"
472+ onClick = { ( ) => setShowAdvanced ( ( prev ) => ! prev ) }
473+ >
474+ { showAdvanced ? "Hide advanced options" : "Advanced options" }
378475 </ button >
379476 </ div >
380477 </ div >
0 commit comments