@@ -31,6 +31,9 @@ export function InstalledScriptsTab() {
3131 const [ editFormData , setEditFormData ] = useState < { script_name : string ; container_id : string } > ( { script_name : '' , container_id : '' } ) ;
3232 const [ showAddForm , setShowAddForm ] = useState ( false ) ;
3333 const [ addFormData , setAddFormData ] = useState < { script_name : string ; container_id : string ; server_id : string } > ( { script_name : '' , container_id : '' , server_id : 'local' } ) ;
34+ const [ showAutoDetectForm , setShowAutoDetectForm ] = useState ( false ) ;
35+ const [ autoDetectServerId , setAutoDetectServerId ] = useState < string > ( '' ) ;
36+ const [ autoDetectStatus , setAutoDetectStatus ] = useState < { type : 'success' | 'error' | null ; message : string } > ( { type : null , message : '' } ) ;
3437
3538 // Fetch installed scripts
3639 const { data : scriptsData , refetch : refetchScripts , isLoading } = api . installedScripts . getAllInstalledScripts . useQuery ( ) ;
@@ -68,6 +71,37 @@ export function InstalledScriptsTab() {
6871 }
6972 } ) ;
7073
74+ // Auto-detect LXC containers mutation
75+ const autoDetectMutation = api . installedScripts . autoDetectLXCContainers . useMutation ( {
76+ onSuccess : ( data ) => {
77+ console . log ( 'Auto-detect success:' , data ) ;
78+ void refetchScripts ( ) ;
79+ setShowAutoDetectForm ( false ) ;
80+ setAutoDetectServerId ( '' ) ;
81+ setAutoDetectStatus ( {
82+ type : 'success' ,
83+ message : data . message || 'Auto-detection completed successfully!'
84+ } ) ;
85+ // Clear status after 5 seconds
86+ setTimeout ( ( ) => setAutoDetectStatus ( { type : null , message : '' } ) , 5000 ) ;
87+ } ,
88+ onError : ( error ) => {
89+ console . error ( 'Auto-detect mutation error:' , error ) ;
90+ console . error ( 'Error details:' , {
91+ message : error . message ,
92+ cause : error . cause ,
93+ stack : error . stack ,
94+ data : error . data
95+ } ) ;
96+ setAutoDetectStatus ( {
97+ type : 'error' ,
98+ message : error . message || 'Auto-detection failed. Please try again.'
99+ } ) ;
100+ // Clear status after 5 seconds
101+ setTimeout ( ( ) => setAutoDetectStatus ( { type : null , message : '' } ) , 5000 ) ;
102+ }
103+ } ) ;
104+
71105
72106 const scripts : InstalledScript [ ] = ( scriptsData ?. scripts as InstalledScript [ ] ) ?? [ ] ;
73107 const stats = statsData ?. stats ;
@@ -197,6 +231,25 @@ export function InstalledScriptsTab() {
197231 setAddFormData ( { script_name : '' , container_id : '' , server_id : 'local' } ) ;
198232 } ;
199233
234+ const handleAutoDetect = ( ) => {
235+ if ( ! autoDetectServerId ) {
236+ return ;
237+ }
238+
239+ if ( autoDetectMutation . isPending ) {
240+ return ;
241+ }
242+
243+ setAutoDetectStatus ( { type : null , message : '' } ) ;
244+ console . log ( 'Starting auto-detect for server ID:' , autoDetectServerId ) ;
245+ autoDetectMutation . mutate ( { serverId : Number ( autoDetectServerId ) } ) ;
246+ } ;
247+
248+ const handleCancelAutoDetect = ( ) => {
249+ setShowAutoDetectForm ( false ) ;
250+ setAutoDetectServerId ( '' ) ;
251+ } ;
252+
200253
201254 const formatDate = ( dateString : string ) => {
202255 return new Date ( dateString ) . toLocaleString ( ) ;
@@ -251,15 +304,22 @@ export function InstalledScriptsTab() {
251304 </ div >
252305 ) }
253306
254- { /* Add Script Button */ }
255- < div className = "mb-4" >
307+ { /* Add Script and Auto-Detect Buttons */ }
308+ < div className = "mb-4 flex flex-col sm:flex-row gap-3 " >
256309 < Button
257310 onClick = { ( ) => setShowAddForm ( ! showAddForm ) }
258311 variant = { showAddForm ? "outline" : "default" }
259312 size = "default"
260313 >
261314 { showAddForm ? 'Cancel Add Script' : '+ Add Manual Script Entry' }
262315 </ Button >
316+ < Button
317+ onClick = { ( ) => setShowAutoDetectForm ( ! showAutoDetectForm ) }
318+ variant = { showAutoDetectForm ? "outline" : "secondary" }
319+ size = "default"
320+ >
321+ { showAutoDetectForm ? 'Cancel Auto-Detect' : '🔍 Auto-Detect LXC Containers (Must contain a tag with "community-script")' }
322+ </ Button >
263323 </ div >
264324
265325 { /* Add Script Form */ }
@@ -331,6 +391,108 @@ export function InstalledScriptsTab() {
331391 </ div >
332392 ) }
333393
394+ { /* Auto-Detect Status Message */ }
395+ { autoDetectStatus . type && (
396+ < div className = { `mb-4 p-4 rounded-lg border ${
397+ autoDetectStatus . type === 'success'
398+ ? 'bg-green-50 dark:bg-green-950/20 border-green-200 dark:border-green-800'
399+ : 'bg-red-50 dark:bg-red-950/20 border-red-200 dark:border-red-800'
400+ } `} >
401+ < div className = "flex items-center" >
402+ < div className = "flex-shrink-0" >
403+ { autoDetectStatus . type === 'success' ? (
404+ < svg className = "h-5 w-5 text-green-400" viewBox = "0 0 20 20" fill = "currentColor" >
405+ < path fillRule = "evenodd" d = "M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule = "evenodd" />
406+ </ svg >
407+ ) : (
408+ < svg className = "h-5 w-5 text-red-400" viewBox = "0 0 20 20" fill = "currentColor" >
409+ < path fillRule = "evenodd" d = "M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule = "evenodd" />
410+ </ svg >
411+ ) }
412+ </ div >
413+ < div className = "ml-3" >
414+ < p className = { `text-sm font-medium ${
415+ autoDetectStatus . type === 'success'
416+ ? 'text-green-800 dark:text-green-200'
417+ : 'text-red-800 dark:text-red-200'
418+ } `} >
419+ { autoDetectStatus . message }
420+ </ p >
421+ </ div >
422+ </ div >
423+ </ div >
424+ ) }
425+
426+ { /* Auto-Detect LXC Containers Form */ }
427+ { showAutoDetectForm && (
428+ < div className = "mb-6 p-4 sm:p-6 bg-card rounded-lg border border-border shadow-sm" >
429+ < h3 className = "text-lg font-semibold text-foreground mb-4 sm:mb-6" > Auto-Detect LXC Containers (Must contain a tag with "community-script")</ h3 >
430+ < div className = "space-y-4 sm:space-y-6" >
431+ < div className = "bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4" >
432+ < div className = "flex items-start" >
433+ < div className = "flex-shrink-0" >
434+ < svg className = "h-5 w-5 text-blue-400" viewBox = "0 0 20 20" fill = "currentColor" >
435+ < path fillRule = "evenodd" d = "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule = "evenodd" />
436+ </ svg >
437+ </ div >
438+ < div className = "ml-3" >
439+ < h4 className = "text-sm font-medium text-blue-800 dark:text-blue-200" >
440+ How it works
441+ </ h4 >
442+ < div className = "mt-2 text-sm text-blue-700 dark:text-blue-300" >
443+ < p > This feature will:</ p >
444+ < ul className = "list-disc list-inside mt-1 space-y-1" >
445+ < li > Connect to the selected server via SSH</ li >
446+ < li > Scan all LXC config files in /etc/pve/lxc/</ li >
447+ < li > Find containers with "community-script" in their tags</ li >
448+ < li > Extract the container ID and hostname</ li >
449+ < li > Add them as installed script entries</ li >
450+ </ ul >
451+ </ div >
452+ </ div >
453+ </ div >
454+ </ div >
455+
456+ < div className = "space-y-2" >
457+ < label className = "block text-sm font-medium text-foreground" >
458+ Select Server *
459+ </ label >
460+ < select
461+ value = { autoDetectServerId }
462+ onChange = { ( e ) => setAutoDetectServerId ( e . target . value ) }
463+ className = "w-full px-3 py-2 border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
464+ >
465+ < option value = "" > Choose a server...</ option >
466+ { serversData ?. servers ?. map ( ( server : any ) => (
467+ < option key = { server . id } value = { server . id } >
468+ { server . name } ({ server . ip } )
469+ </ option >
470+ ) ) }
471+ </ select >
472+ </ div >
473+ </ div >
474+ < div className = "flex flex-col sm:flex-row justify-end gap-3 mt-4 sm:mt-6" >
475+ < Button
476+ onClick = { handleCancelAutoDetect }
477+ variant = "outline"
478+ size = "default"
479+ className = "w-full sm:w-auto"
480+ >
481+ Cancel
482+ </ Button >
483+ < Button
484+ onClick = { handleAutoDetect }
485+ disabled = { autoDetectMutation . isPending || ! autoDetectServerId }
486+ variant = "default"
487+ size = "default"
488+ className = "w-full sm:w-auto"
489+ >
490+ { autoDetectMutation . isPending ? '🔍 Scanning...' : '🔍 Start Auto-Detection' }
491+ </ Button >
492+ </ div >
493+ </ div >
494+ ) }
495+
334496 { /* Filters */ }
335497 < div className = "space-y-4" >
336498 { /* Search Input - Full Width on Mobile */ }
0 commit comments