11import React , { useCallback , useEffect , useRef , useState } from 'react' ;
2- import { scanRepository , searchLibraries , createLibrary } from '../api/client.js' ;
2+ import { cloneRepository , listRepositoryPackages , searchLibraries , createLibrary } from '../api/client.js' ;
33
44const RiskBar = ( { score, explanation } ) => {
55 if ( score === undefined || score === null || Number . isNaN ( score ) ) return null ;
@@ -22,6 +22,7 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
2222 const [ error , setError ] = useState ( null ) ;
2323 const [ loading , setLoading ] = useState ( false ) ;
2424 const [ files , setFiles ] = useState ( [ ] ) ;
25+ const [ statusMessage , setStatusMessage ] = useState ( '' ) ;
2526 const [ depJobs , setDepJobs ] = useState ( [ ] ) ;
2627 const [ processing , setProcessing ] = useState ( false ) ;
2728 const inputRef = useRef ( null ) ;
@@ -38,6 +39,9 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
3839 return '(N/A)' ;
3940 } ;
4041
42+ const totalJobs = depJobs . length ;
43+ const processedJobs = depJobs . filter ( j => j . status !== 'pending' ) . length ;
44+
4145 const resetState = ( ) => {
4246 setRepoUrl ( '' ) ;
4347 setError ( null ) ;
@@ -74,6 +78,13 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
7478 return cleaned || null ;
7579 } , [ ] ) ;
7680
81+ const cancelledRef = React . useRef ( false ) ;
82+ useEffect ( ( ) => {
83+ return ( ) => {
84+ cancelledRef . current = true ;
85+ } ;
86+ } , [ ] ) ;
87+
7788 const computeRisk = useCallback ( ( match = { } ) => {
7889 const summaries = Array . isArray ( match . licenseSummary ?? match . license_summary )
7990 ? ( match . licenseSummary ?? match . license_summary )
@@ -123,9 +134,18 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
123134 setError ( null ) ;
124135 setFiles ( [ ] ) ;
125136 setDepJobs ( [ ] ) ;
126- const res = await scanRepository ( repoUrl ) ;
127- const scannedFiles = res . files ?? [ ] ;
137+
138+ setStatusMessage ( 'Cloning repository...' ) ;
139+ const cloneRes = await cloneRepository ( repoUrl ) ;
140+ setStatusMessage ( 'Cloning repository completed.' ) ;
141+ const root = cloneRes . root ;
142+
143+ setStatusMessage ( 'Listing dependency files...' ) ;
144+ const listRes = await listRepositoryPackages ( { root } ) ;
145+ const scannedFiles = listRes . files ?? cloneRes . files ?? [ ] ;
128146 setFiles ( scannedFiles ) ;
147+
148+ setStatusMessage ( 'Scanning dependencies...' ) ;
129149 const jobs = [ ] ;
130150 scannedFiles . forEach ( ( file , fIdx ) => {
131151 const deps = Array . isArray ( file ?. report ?. dependencies ) ? file . report . dependencies : [ ] ;
@@ -145,24 +165,154 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
145165 } ) ;
146166 } ) ;
147167 setDepJobs ( jobs ) ;
168+ // Process jobs sequentially: check local DB, fallback to MCP, persist if needed
169+ setProcessing ( true ) ;
170+ const updateJob = ( id , patch ) =>
171+ setDepJobs ( prev => prev . map ( j => ( j . id === id ? { ...j , ...patch } : j ) ) ) ;
172+
173+ for ( const next of jobs ) {
174+ if ( cancelledRef . current ) break ;
175+ const q = next . version ? `${ next . name } ${ next . version } ` : next . name ;
176+ try {
177+ updateJob ( next . id , { status : 'searching' , message : null } ) ;
178+ const res = await searchLibraries ( q ) ;
179+ let match = null ;
180+ let existing = false ;
181+
182+ if ( res ?. source === 'mongo' && Array . isArray ( res ?. results ) && res . results . length > 0 ) {
183+ existing = true ;
184+ const lib = res . results [ 0 ] ;
185+ const v = lib . versions ?. [ 0 ] ;
186+ match = {
187+ name : lib . name ,
188+ version : v ?. version ,
189+ ecosystem : lib . ecosystem ,
190+ description : lib . description ,
191+ repository : lib . repository_url ,
192+ license : v ?. license_name ,
193+ license_url : v ?. license_url ,
194+ licenseSummary : v ?. license_summary ?? [ ] ,
195+ evidence : v ?. evidence ?? [ ] ,
196+ confidence : v ?. confidence ,
197+ risk_level : v ?. risk_level ,
198+ risk_score : v ?. risk_score ,
199+ risk_score_explanation : v ?. risk_score_explanation
200+ } ;
201+ } else if ( res ?. source === 'mcp' && Array . isArray ( res ?. results ) && res . results . length > 0 ) {
202+ const lib = res . results [ 0 ] ;
203+ const v = lib . versions ?. [ 0 ] ;
204+ match = {
205+ name : lib . name ,
206+ version : v ?. version ,
207+ ecosystem : lib . ecosystem ,
208+ description : lib . description ,
209+ repository : lib . repository_url ,
210+ license : v ?. license_name ,
211+ license_url : v ?. license_url ,
212+ licenseSummary : v ?. license_summary ?? [ ] ,
213+ evidence : v ?. evidence ?? [ ] ,
214+ confidence : v ?. confidence ,
215+ risk_level : v ?. risk_level ,
216+ risk_score : v ?. risk_score ,
217+ risk_score_explanation : v ?. risk_score_explanation ,
218+ officialSite : lib . officialSite
219+ } ;
220+ } else if ( res ?. discovery ?. matches ?. length ) {
221+ match = res . discovery . bestMatch ?? res . discovery . matches [ 0 ] ;
222+ }
223+
224+ if ( ! match ) {
225+ updateJob ( next . id , { status : 'error' , message : 'Eşleşme bulunamadı' } ) ;
226+ continue ;
227+ }
228+
229+ const computedRisk = computeRisk ( match ) ;
230+ const risk = {
231+ level : match . risk_level ?? computedRisk . level ,
232+ score : match . risk_score ?? computedRisk . score ,
233+ explanation : match . risk_score_explanation ?? computedRisk . explanation
234+ } ;
235+
236+ if ( existing || res ?. source === 'mongo' ) {
237+ updateJob ( next . id , {
238+ status : 'done' ,
239+ message : 'Zaten kayıtlı' ,
240+ match,
241+ risk_level : risk . level ,
242+ risk_score : risk . score ,
243+ risk_score_explanation : risk . explanation
244+ } ) ;
245+ continue ;
246+ }
247+
248+ updateJob ( next . id , { status : 'importing' , match, risk_level : risk . level , risk_score : risk . score , risk_score_explanation : risk . explanation } ) ;
249+
250+ const payload = {
251+ name : match . name ?? next . name ,
252+ ecosystem : match . ecosystem ?? res ?. discovery ?. query ?. ecosystem ?? 'unknown' ,
253+ description : match . description ,
254+ repository_url : match . repository ?? match . officialSite ?? null ,
255+ officialSite : match . officialSite ?? match . repository ?? null ,
256+ versions : [
257+ {
258+ version : normalizeVersion ( match . version ?? next . version ) ?? 'unknown' ,
259+ license_name : match . license ?? null ,
260+ license_url : match . license_url ?? null ,
261+ notes : match . summary ?? null ,
262+ license_summary : Array . isArray ( match . licenseSummary )
263+ ? match . licenseSummary
264+ . map ( item =>
265+ typeof item === 'object' && item !== null
266+ ? { summary : item . summary ?? '' , emoji : item . emoji ?? null }
267+ : { summary : item , emoji : null }
268+ )
269+ . filter ( entry => typeof entry . summary === 'string' && entry . summary . length > 0 )
270+ : [ ] ,
271+ confidence : match . confidence ?? null ,
272+ evidence : Array . isArray ( match . evidence ) ? match . evidence : [ ] ,
273+ risk_level : risk . level ,
274+ risk_score : risk . score ,
275+ risk_score_explanation : risk . explanation
276+ }
277+ ]
278+ } ;
279+
280+ await createLibrary ( payload ) ;
281+ updateJob ( next . id , { status : 'done' , message : 'Eklendi' , match } ) ;
282+ if ( onImported ) onImported ( ) ;
283+ } catch ( err ) {
284+ updateJob ( next . id , { status : 'error' , message : err ?. message ?? String ( err ) } ) ;
285+ }
286+ }
287+
288+ setProcessing ( false ) ;
148289 } catch ( err ) {
149- setError ( err . message ) ;
290+ setError ( err ? .message ?? String ( err ) ) ;
150291 } finally {
151292 setLoading ( false ) ;
152293 }
153294 } ;
154295
155296 useEffect ( ( ) => {
297+ let cancelled = false ;
298+
156299 const processNext = async ( ) => {
300+ if ( cancelled ) return ;
157301 if ( processing ) return ;
302+
158303 const next = depJobs . find ( job => job . status === 'pending' ) ;
159304 if ( ! next ) return ;
305+
160306 setProcessing ( true ) ;
161307 const updateJob = ( id , patch ) =>
162308 setDepJobs ( jobs => jobs . map ( j => ( j . id === id ? { ...j , ...patch } : j ) ) ) ;
309+
163310 const q = next . version ? `${ next . name } ${ next . version } ` : next . name ;
311+
164312 try {
165313 updateJob ( next . id , { status : 'searching' , message : null } ) ;
314+
315+ // 1) Check local DB
166316 const res = await searchLibraries ( q ) ;
167317 let match = null ;
168318 let existing = false ;
@@ -187,6 +337,7 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
187337 risk_score_explanation : v ?. risk_score_explanation
188338 } ;
189339 } else if ( res ?. source === 'mcp' && Array . isArray ( res ?. results ) && res . results . length > 0 ) {
340+ // MCP returned direct results
190341 const lib = res . results [ 0 ] ;
191342 const v = lib . versions ?. [ 0 ] ;
192343 match = {
@@ -206,6 +357,7 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
206357 officialSite : lib . officialSite
207358 } ;
208359 } else if ( res ?. discovery ?. matches ?. length ) {
360+ // discovery bestMatch or matches
209361 match = res . discovery . bestMatch ?? res . discovery . matches [ 0 ] ;
210362 }
211363
@@ -235,7 +387,9 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
235387 return ;
236388 }
237389
390+ // persist MCP/discovery result into local DB
238391 updateJob ( next . id , { status : 'importing' , match, risk_level : risk . level , risk_score : risk . score , risk_score_explanation : risk . explanation } ) ;
392+
239393 const payload = {
240394 name : match . name ?? next . name ,
241395 ecosystem : match . ecosystem ?? res ?. discovery ?. query ?. ecosystem ?? 'unknown' ,
@@ -250,12 +404,12 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
250404 notes : match . summary ?? null ,
251405 license_summary : Array . isArray ( match . licenseSummary )
252406 ? match . licenseSummary
253- . map ( item =>
254- typeof item === 'object' && item !== null
255- ? { summary : item . summary ?? '' , emoji : item . emoji ?? null }
256- : { summary : item , emoji : null }
257- )
258- . filter ( entry => typeof entry . summary === 'string' && entry . summary . length > 0 )
407+ . map ( item =>
408+ typeof item === 'object' && item !== null
409+ ? { summary : item . summary ?? '' , emoji : item . emoji ?? null }
410+ : { summary : item , emoji : null }
411+ )
412+ . filter ( entry => typeof entry . summary === 'string' && entry . summary . length > 0 )
259413 : [ ] ,
260414 confidence : match . confidence ?? null ,
261415 evidence : Array . isArray ( match . evidence ) ? match . evidence : [ ] ,
@@ -270,12 +424,19 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
270424 updateJob ( next . id , { status : 'done' , message : 'Eklendi' , match } ) ;
271425 if ( onImported ) onImported ( ) ;
272426 } catch ( err ) {
273- updateJob ( next . id , { status : 'error' , message : err . message } ) ;
427+ updateJob ( next . id , { status : 'error' , message : err ? .message ?? String ( err ) } ) ;
274428 } finally {
429+ setStatusMessage ( 'Scanning completed.' ) ;
275430 setProcessing ( false ) ;
276431 }
277432 } ;
433+
434+ // try to drive processing whenever jobs change
278435 processNext ( ) ;
436+
437+ return ( ) => {
438+ cancelled = true ;
439+ } ;
279440 } , [ depJobs , processing , computeRisk , normalizeVersion , onImported ] ) ;
280441
281442 if ( ! isOpen ) return null ;
@@ -296,11 +457,11 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
296457 ✕
297458 </ button >
298459 </ div >
299- < div className = "panel" style = { { background : 'transparent' , boxShadow : 'none' , color : 'white' } } >
460+ < div className = "panel" style = { { background : 'transparent' , boxShadow : 'none' , color : 'white' , overflowY : 'visible' } } >
300461 < p > Repo linkini girin, link geçerliyse tarayalım.</ p >
301462 < form
302463 className = "inline-form"
303- style = { { marginBottom : '1rem ' } }
464+ style = { { marginBottom : '0.5rem ' } }
304465 onSubmit = { handleSubmit }
305466 >
306467 < input
@@ -311,15 +472,21 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
311472 ref = { inputRef }
312473 />
313474 < div style = { { textAlign : 'right' } } >
314- < button type = "submit" disabled = { Boolean ( error ) || ! repoUrl || loading } className = "button" >
315- { loading ? 'Taranıyor…' : 'Kontrol' }
475+ < button type = "submit" disabled = { Boolean ( error ) || ! repoUrl || loading || processing } className = "button" >
476+ { ( loading || processing ) ? 'Taranıyor…' : 'Kontrol Et ' }
316477 </ button >
317478 </ div >
318479 </ form >
319480 { error && < p className = "error" > { error } </ p > }
481+ { statusMessage && (
482+ < div style = { { marginBottom : '0.5rem' , color : '#cbd5f5' } } >
483+ { statusMessage } { ( loading || processing ) && totalJobs > 0 ? ` (${ processedJobs } /${ totalJobs } )` : '' }
484+ </ div >
485+ ) }
320486 { files . length > 0 && (
321- < div style = { { marginTop : '1rem' , background : 'rgba(255,255,255,0.05)' , padding : '0.75rem' , borderRadius : '12px' } } >
487+ < div style = { { marginTop : '1rem' , background : 'rgba(255,255,255,0.05)' , padding : '0.75rem' , borderRadius : '12px' , maxHeight : '320px' , overflowY : 'auto' , WebkitOverflowScrolling : 'touch' } } >
322488 < p style = { { marginTop : 0 , marginBottom : '0.5rem' } } > Bulunan dependency dosyaları:</ p >
489+
323490 < ul style = { { margin : 0 , paddingLeft : 0 , color : 'white' , listStyle : 'none' , display : 'flex' , flexDirection : 'column' , gap : '0.6rem' } } >
324491 { files . map ( ( f , idx ) => {
325492 const deps = jobsByFile ( f . path ) ;
0 commit comments