@@ -125,7 +125,9 @@ export function GlobalEnrichmentSourceManagement({
125125 key : source . key ,
126126 displayName : source . displayName ,
127127 enabled : source . enabled ,
128+ targets : source . targets ,
128129 refreshTtlHours : source . refreshTtlHours ,
130+ options : source . options ,
129131 credentials : {
130132 storedCredentialId : source . credentials . storedCredentialId ?? null ,
131133 secret : source . credentials . secret ,
@@ -285,6 +287,18 @@ export function GlobalEnrichmentSourceManagement({
285287 { source . key }
286288 </ span >
287289 </ div >
290+ { source . targets . length > 0 ? (
291+ < div className = "mt-1 flex flex-wrap gap-1 pl-8" >
292+ { source . targets . map ( ( target ) => (
293+ < span
294+ key = { target }
295+ className = "rounded-full border border-border/50 bg-background/60 px-2 py-0.5 text-[10px] text-muted-foreground"
296+ >
297+ { formatTargetLabel ( target ) }
298+ </ span >
299+ ) ) }
300+ </ div >
301+ ) : null }
288302 { source . runtime . lastError ? (
289303 < p
290304 className = "mt-0.5 max-w-[200px] truncate pl-8 text-[11px] text-destructive"
@@ -537,7 +551,7 @@ function EnrichmentSourceEditorSheetContent({
537551 < div className = "space-y-0.5" >
538552 < p className = "text-sm font-medium" > Enable provider</ p >
539553 < p className = "text-xs text-muted-foreground" >
540- When enabled, the worker will invoke this enrichment source during vulnerability processing .
554+ When enabled, selected PatchHound processes can invoke this enrichment source.
541555 </ p >
542556 </ div >
543557 < input
@@ -551,6 +565,36 @@ function EnrichmentSourceEditorSheetContent({
551565 }
552566 />
553567 </ label >
568+
569+ < div className = "grid gap-2 rounded-lg border border-border/60 px-4 py-3" >
570+ < p className = "text-sm font-medium" > Process targets</ p >
571+ < label className = "flex items-center gap-2 text-sm text-muted-foreground" >
572+ < input
573+ type = "checkbox"
574+ checked = { source . targets . some ( ( target ) => target . toLowerCase ( ) === 'scheduled' ) }
575+ onChange = { ( event ) =>
576+ onUpdateSource ( source . key , ( current ) => ( {
577+ ...current ,
578+ targets : updateTargetList ( current . targets , 'Scheduled' , event . target . checked ) ,
579+ } ) )
580+ }
581+ />
582+ Scheduled enrichment worker
583+ </ label >
584+ < label className = "flex items-center gap-2 text-sm text-muted-foreground" >
585+ < input
586+ type = "checkbox"
587+ checked = { source . targets . some ( ( target ) => target . toLowerCase ( ) === 'airesearch' ) }
588+ onChange = { ( event ) =>
589+ onUpdateSource ( source . key , ( current ) => ( {
590+ ...current ,
591+ targets : updateTargetList ( current . targets , 'AIResearch' , event . target . checked ) ,
592+ } ) )
593+ }
594+ />
595+ AI research and assessments
596+ </ label >
597+ </ div >
554598 </ FormSection >
555599
556600 < FormSection title = "Configuration" >
@@ -610,14 +654,139 @@ function EnrichmentSourceEditorSheetContent({
610654 </ div >
611655 </ FormSection >
612656
657+ { source . key === 'jina-reader' && source . options . jinaReader ? (
658+ < FormSection title = "Jina Reader options" >
659+ < div className = "grid gap-4 sm:grid-cols-2" >
660+ < div className = "grid content-start gap-2" >
661+ < span className = "text-[11px] uppercase tracking-[0.14em] text-muted-foreground" > Timeout Seconds</ span >
662+ < Input
663+ type = "number"
664+ min = { 3 }
665+ max = { 60 }
666+ value = { source . options . jinaReader . timeoutSeconds }
667+ onChange = { ( event ) =>
668+ onUpdateJinaReaderOption ( source , onUpdateSource , 'timeoutSeconds' , Number ( event . target . value || 3 ) )
669+ }
670+ className = "h-10"
671+ />
672+ </ div >
673+ < div className = "grid content-start gap-2" >
674+ < span className = "text-[11px] uppercase tracking-[0.14em] text-muted-foreground" > Max Content Chars</ span >
675+ < Input
676+ type = "number"
677+ min = { 1000 }
678+ max = { 20000 }
679+ step = { 500 }
680+ value = { source . options . jinaReader . maxContentChars }
681+ onChange = { ( event ) =>
682+ onUpdateJinaReaderOption ( source , onUpdateSource , 'maxContentChars' , Number ( event . target . value || 1000 ) )
683+ }
684+ className = "h-10"
685+ />
686+ </ div >
687+ < div className = "grid content-start gap-2" >
688+ < span className = "text-[11px] uppercase tracking-[0.14em] text-muted-foreground" > Response Format</ span >
689+ < Select
690+ value = { source . options . jinaReader . responseFormat }
691+ onValueChange = { ( value ) => onUpdateJinaReaderOption ( source , onUpdateSource , 'responseFormat' , value ?? 'markdown' ) }
692+ >
693+ < SelectTrigger className = "h-10 w-full rounded-lg border-border/70 bg-background px-3" >
694+ < SelectValue />
695+ </ SelectTrigger >
696+ < SelectContent className = "rounded-xl border-border/70 bg-popover/95 backdrop-blur" >
697+ < SelectItem value = "markdown" > Markdown</ SelectItem >
698+ < SelectItem value = "text" > Text</ SelectItem >
699+ < SelectItem value = "html" > HTML</ SelectItem >
700+ </ SelectContent >
701+ </ Select >
702+ </ div >
703+ < div className = "grid content-start gap-2" >
704+ < span className = "text-[11px] uppercase tracking-[0.14em] text-muted-foreground" > Reader Engine</ span >
705+ < label className = "flex h-10 items-center gap-2 rounded-lg border border-border/70 bg-background px-3 text-sm text-muted-foreground" >
706+ < input
707+ type = "checkbox"
708+ checked = { source . options . jinaReader . useReaderLmV2 }
709+ onChange = { ( event ) =>
710+ onUpdateJinaReaderOption ( source , onUpdateSource , 'useReaderLmV2' , event . target . checked )
711+ }
712+ />
713+ Use ReaderLM v2
714+ </ label >
715+ </ div >
716+ < div className = "grid content-start gap-2" >
717+ < span className = "text-[11px] uppercase tracking-[0.14em] text-muted-foreground" > Target Selector</ span >
718+ < Input
719+ value = { source . options . jinaReader . targetSelector }
720+ onChange = { ( event ) =>
721+ onUpdateJinaReaderOption ( source , onUpdateSource , 'targetSelector' , event . target . value )
722+ }
723+ className = "h-10"
724+ />
725+ </ div >
726+ < div className = "grid content-start gap-2" >
727+ < span className = "text-[11px] uppercase tracking-[0.14em] text-muted-foreground" > Exclude Selector</ span >
728+ < Input
729+ value = { source . options . jinaReader . excludeSelector }
730+ onChange = { ( event ) =>
731+ onUpdateJinaReaderOption ( source , onUpdateSource , 'excludeSelector' , event . target . value )
732+ }
733+ className = "h-10"
734+ />
735+ </ div >
736+ < div className = "grid content-start gap-2" >
737+ < span className = "text-[11px] uppercase tracking-[0.14em] text-muted-foreground" > Wait For Selector</ span >
738+ < Input
739+ value = { source . options . jinaReader . waitForSelector }
740+ onChange = { ( event ) =>
741+ onUpdateJinaReaderOption ( source , onUpdateSource , 'waitForSelector' , event . target . value )
742+ }
743+ className = "h-10"
744+ />
745+ </ div >
746+ < div className = "grid gap-2" >
747+ < label className = "flex items-center gap-2 text-sm text-muted-foreground" >
748+ < input
749+ type = "checkbox"
750+ checked = { source . options . jinaReader . removeImages }
751+ onChange = { ( event ) =>
752+ onUpdateJinaReaderOption ( source , onUpdateSource , 'removeImages' , event . target . checked )
753+ }
754+ />
755+ Remove images
756+ </ label >
757+ < label className = "flex items-center gap-2 text-sm text-muted-foreground" >
758+ < input
759+ type = "checkbox"
760+ checked = { source . options . jinaReader . includeLinkSummary }
761+ onChange = { ( event ) =>
762+ onUpdateJinaReaderOption ( source , onUpdateSource , 'includeLinkSummary' , event . target . checked )
763+ }
764+ />
765+ Include link summary
766+ </ label >
767+ < label className = "flex items-center gap-2 text-sm text-muted-foreground" >
768+ < input
769+ type = "checkbox"
770+ checked = { source . options . jinaReader . includeImageSummary }
771+ onChange = { ( event ) =>
772+ onUpdateJinaReaderOption ( source , onUpdateSource , 'includeImageSummary' , event . target . checked )
773+ }
774+ />
775+ Include image summary
776+ </ label >
777+ </ div >
778+ </ div >
779+ </ FormSection >
780+ ) : null }
781+
613782 < FormSection title = "Credentials" >
614783 { source . credentials . acceptedCredentialTypes . length > 0 ? (
615784 < StoredCredentialSelector
616785 source = { source }
617786 storedCredentials = { storedCredentials }
618787 onUpdateSource = { onUpdateSource }
619788 />
620- ) : source . credentialMode === 'global-secret' || source . key === 'nvd' ? (
789+ ) : source . credentialMode === 'global-secret' || source . credentialMode === 'optional-global-secret' || source . key === 'nvd' ? (
621790 < div className = "grid content-start gap-2" >
622791 < span className = "text-[11px] uppercase tracking-[0.14em] text-muted-foreground" >
623792 API Key{ source . key === 'nvd' ? ' (optional)' : '' }
@@ -859,7 +1028,7 @@ function StoredCredentialSelector({
8591028 Enter a provider-specific secret below, or select a reusable global credential.
8601029 </ p >
8611030 ) }
862- { source . credentialMode === 'global-secret' || source . key === 'nvd' ? (
1031+ { source . credentialMode === 'global-secret' || source . credentialMode === 'optional-global-secret' || source . key === 'nvd' ? (
8631032 < Input
8641033 type = "password"
8651034 placeholder = {
@@ -893,6 +1062,44 @@ function mapSourceToDraft(source: EnrichmentSource): EnrichmentSourceDraft {
8931062 }
8941063}
8951064
1065+ function updateTargetList ( targets : string [ ] , target : string , enabled : boolean ) {
1066+ const normalized = targets . filter ( ( item ) => item . toLowerCase ( ) !== target . toLowerCase ( ) )
1067+ return enabled ? [ ...normalized , target ] : normalized
1068+ }
1069+
1070+ function formatTargetLabel ( target : string ) {
1071+ switch ( target . toLowerCase ( ) ) {
1072+ case 'airesearch' :
1073+ return 'AI research'
1074+ case 'scheduled' :
1075+ return 'Scheduled'
1076+ default :
1077+ return target
1078+ }
1079+ }
1080+
1081+ function onUpdateJinaReaderOption < TKey extends keyof NonNullable < EnrichmentSource [ 'options' ] [ 'jinaReader' ] > > (
1082+ source : EnrichmentSourceDraft ,
1083+ onUpdateSource : ( key : string , mutate : ( current : EnrichmentSourceDraft ) => EnrichmentSourceDraft ) => void ,
1084+ key : TKey ,
1085+ value : NonNullable < EnrichmentSource [ 'options' ] [ 'jinaReader' ] > [ TKey ] ,
1086+ ) {
1087+ onUpdateSource ( source . key , ( current ) => {
1088+ if ( ! current . options . jinaReader ) return current
1089+
1090+ return {
1091+ ...current ,
1092+ options : {
1093+ ...current . options ,
1094+ jinaReader : {
1095+ ...current . options . jinaReader ,
1096+ [ key ] : value ,
1097+ } ,
1098+ } ,
1099+ }
1100+ } )
1101+ }
1102+
8961103function formatTimestamp ( value : string | null ) {
8971104 if ( ! value ) return 'Never'
8981105 const date = new Date ( value )
@@ -936,6 +1143,11 @@ function getProviderStatusDescription(source: EnrichmentSource) {
9361143 return 'Enriches software items with end-of-life date and support status from the endoflife.date database.'
9371144 }
9381145
1146+ if ( source . key === 'jina-reader' ) {
1147+ if ( ! source . enabled ) return 'Jina Reader is configured but currently inactive.'
1148+ return 'Jina Reader can fetch public web pages as research context for AI assessments. API keys are optional.'
1149+ }
1150+
9391151 return source . enabled
9401152 ? 'This shared provider is available to enrich tenant vulnerability data during worker processing.'
9411153 : 'This shared provider is configured but not currently used by the worker.'
0 commit comments