@@ -458,14 +458,118 @@ async function isVM(scriptId: number, containerId: string, serverId: number | nu
458458 ) ;
459459 } ) ;
460460
461- // If LXC config exists, it's an LXC container
462- return ! lxcConfigExists ; // Return true if it's a VM (neither config exists defaults to false/LXC)
461+
462+ return false ; // Always LXC since VM config doesn't exist
463463 } catch ( error ) {
464464 console . error ( 'Error determining container type:' , error ) ;
465465 return false ; // Default to LXC on error
466466 }
467467}
468468
469+ // Helper function to batch detect container types for all containers on a server
470+ // Returns a Map of container_id -> isVM (true for VM, false for LXC)
471+ async function batchDetectContainerTypes ( server : Server ) : Promise < Map < string , boolean > > {
472+ const containerTypeMap = new Map < string , boolean > ( ) ;
473+
474+ try {
475+ // Import SSH services
476+ const { default : SSHService } = await import ( '~/server/ssh-service' ) ;
477+ const { default : SSHExecutionService } = await import ( '~/server/ssh-execution-service' ) ;
478+ const sshService = new SSHService ( ) ;
479+ const sshExecutionService = new SSHExecutionService ( ) ;
480+
481+ // Test SSH connection first
482+ const connectionTest = await sshService . testSSHConnection ( server ) ;
483+ if ( ! ( connectionTest as any ) . success ) {
484+ console . warn ( `SSH connection failed for server ${ server . name } , skipping batch detection` ) ;
485+ return containerTypeMap ; // Return empty map if SSH fails
486+ }
487+
488+ // Helper function to parse list output and extract IDs
489+ const parseListOutput = ( output : string ) : string [ ] => {
490+ const ids : string [ ] = [ ] ;
491+ const lines = output . split ( '\n' ) . filter ( line => line . trim ( ) ) ;
492+
493+ for ( const line of lines ) {
494+ // Skip header lines
495+ if ( line . includes ( 'VMID' ) || line . includes ( 'CTID' ) ) continue ;
496+
497+ // Extract first column (ID)
498+ const parts = line . trim ( ) . split ( / \s + / ) ;
499+ if ( parts . length > 0 ) {
500+ const id = parts [ 0 ] ?. trim ( ) ;
501+ // Validate ID format (3-4 digits typically)
502+ if ( id && / ^ \d { 3 , 4 } $ / . test ( id ) ) {
503+ ids . push ( id ) ;
504+ }
505+ }
506+ }
507+
508+ return ids ;
509+ } ;
510+
511+ // Get containers from pct list
512+ let pctOutput = '' ;
513+ await new Promise < void > ( ( resolve , reject ) => {
514+ void sshExecutionService . executeCommand (
515+ server ,
516+ 'pct list' ,
517+ ( data : string ) => {
518+ pctOutput += data ;
519+ } ,
520+ ( error : string ) => {
521+ console . error ( `pct list error for server ${ server . name } :` , error ) ;
522+ // Don't reject, just continue - might be no containers
523+ resolve ( ) ;
524+ } ,
525+ ( _exitCode : number ) => {
526+ resolve ( ) ;
527+ }
528+ ) ;
529+ } ) ;
530+
531+ // Get VMs from qm list
532+ let qmOutput = '' ;
533+ await new Promise < void > ( ( resolve , reject ) => {
534+ void sshExecutionService . executeCommand (
535+ server ,
536+ 'qm list' ,
537+ ( data : string ) => {
538+ qmOutput += data ;
539+ } ,
540+ ( error : string ) => {
541+ console . error ( `qm list error for server ${ server . name } :` , error ) ;
542+ // Don't reject, just continue - might be no VMs
543+ resolve ( ) ;
544+ } ,
545+ ( _exitCode : number ) => {
546+ resolve ( ) ;
547+ }
548+ ) ;
549+ } ) ;
550+
551+ // Parse IDs from both lists
552+ const containerIds = parseListOutput ( pctOutput ) ;
553+ const vmIds = parseListOutput ( qmOutput ) ;
554+
555+ // Mark all LXC containers as false (not VM)
556+ for ( const id of containerIds ) {
557+ containerTypeMap . set ( id , false ) ;
558+ }
559+
560+ // Mark all VMs as true (is VM)
561+ for ( const id of vmIds ) {
562+ containerTypeMap . set ( id , true ) ;
563+ }
564+
565+ } catch ( error ) {
566+ console . error ( `Error in batchDetectContainerTypes for server ${ server . name } :` , error ) ;
567+ // Return empty map on error - individual checks will fall back to isVM()
568+ }
569+
570+ return containerTypeMap ;
571+ }
572+
469573
470574export const installedScriptsRouter = createTRPCRouter ( {
471575 // Get all installed scripts
@@ -475,13 +579,52 @@ export const installedScriptsRouter = createTRPCRouter({
475579 const db = getDatabase ( ) ;
476580 const scripts = await db . getAllInstalledScripts ( ) ;
477581
582+ // Group scripts by server_id for batch detection
583+ const scriptsByServer = new Map < number , any [ ] > ( ) ;
584+ const serversMap = new Map < number , Server > ( ) ;
585+
586+ for ( const script of scripts ) {
587+ if ( script . server_id && script . server ) {
588+ if ( ! scriptsByServer . has ( script . server_id ) ) {
589+ scriptsByServer . set ( script . server_id , [ ] ) ;
590+ serversMap . set ( script . server_id , script . server as Server ) ;
591+ }
592+ scriptsByServer . get ( script . server_id ) ! . push ( script ) ;
593+ }
594+ }
595+
596+ // Batch detect container types for each server
597+ const containerTypeMap = new Map < string , boolean > ( ) ;
598+ const batchDetectionPromises = Array . from ( serversMap . entries ( ) ) . map ( async ( [ serverId , server ] ) => {
599+ try {
600+ const serverTypeMap = await batchDetectContainerTypes ( server ) ;
601+ // Merge into main map with server-specific prefix to avoid collisions
602+ // Actually, container IDs are unique across the cluster, so we can use them directly
603+ for ( const [ containerId , isVM ] of serverTypeMap . entries ( ) ) {
604+ containerTypeMap . set ( containerId , isVM ) ;
605+ }
606+ } catch ( error ) {
607+ console . error ( `Error batch detecting types for server ${ serverId } :` , error ) ;
608+ // Continue with other servers
609+ }
610+ } ) ;
611+
612+ await Promise . all ( batchDetectionPromises ) ;
613+
478614 // Transform scripts to flatten server data for frontend compatibility
479-
480- const transformedScripts = await Promise . all ( scripts . map ( async ( script : any ) => {
481- // Determine if it's a VM or LXC
615+ const transformedScripts = scripts . map ( ( script : any ) => {
616+ // Determine if it's a VM or LXC from batch detection map, fall back to isVM() if not found
482617 let is_vm = false ;
483618 if ( script . container_id && script . server_id ) {
484- is_vm = await isVM ( script . id , script . container_id , script . server_id ) ;
619+ // First check if we have it in the batch detection map
620+ if ( containerTypeMap . has ( script . container_id ) ) {
621+ is_vm = containerTypeMap . get ( script . container_id ) ?? false ;
622+ } else {
623+ // Fall back to checking LXCConfig in database (fast, no SSH needed)
624+ // If LXCConfig exists, it's an LXC container
625+ const hasLXCConfig = script . lxc_config !== null && script . lxc_config !== undefined ;
626+ is_vm = ! hasLXCConfig ; // If no LXCConfig, might be VM, but default to false for safety
627+ }
485628 }
486629
487630 return {
@@ -498,7 +641,7 @@ export const installedScriptsRouter = createTRPCRouter({
498641 is_vm,
499642 server : undefined // Remove nested server object
500643 } ;
501- } ) ) ;
644+ } ) ;
502645
503646 return {
504647 success : true ,
@@ -522,13 +665,31 @@ export const installedScriptsRouter = createTRPCRouter({
522665 const db = getDatabase ( ) ;
523666 const scripts = await db . getInstalledScriptsByServer ( input . serverId ) ;
524667
668+ // Batch detect container types for this server
669+ let containerTypeMap = new Map < string , boolean > ( ) ;
670+ if ( scripts . length > 0 && scripts [ 0 ] ?. server ) {
671+ try {
672+ containerTypeMap = await batchDetectContainerTypes ( scripts [ 0 ] . server as Server ) ;
673+ } catch ( error ) {
674+ console . error ( `Error batch detecting types for server ${ input . serverId } :` , error ) ;
675+ // Continue with empty map, will fall back to LXCConfig check
676+ }
677+ }
678+
525679 // Transform scripts to flatten server data for frontend compatibility
526-
527- const transformedScripts = await Promise . all ( scripts . map ( async ( script : any ) => {
528- // Determine if it's a VM or LXC
680+ const transformedScripts = scripts . map ( ( script : any ) => {
681+ // Determine if it's a VM or LXC from batch detection map, fall back to LXCConfig check if not found
529682 let is_vm = false ;
530683 if ( script . container_id && script . server_id ) {
531- is_vm = await isVM ( script . id , script . container_id , script . server_id ) ;
684+ // First check if we have it in the batch detection map
685+ if ( containerTypeMap . has ( script . container_id ) ) {
686+ is_vm = containerTypeMap . get ( script . container_id ) ?? false ;
687+ } else {
688+ // Fall back to checking LXCConfig in database (fast, no SSH needed)
689+ // If LXCConfig exists, it's an LXC container
690+ const hasLXCConfig = script . lxc_config !== null && script . lxc_config !== undefined ;
691+ is_vm = ! hasLXCConfig ; // If no LXCConfig, might be VM, but default to false for safety
692+ }
532693 }
533694
534695 return {
@@ -545,7 +706,7 @@ export const installedScriptsRouter = createTRPCRouter({
545706 is_vm,
546707 server : undefined // Remove nested server object
547708 } ;
548- } ) ) ;
709+ } ) ;
549710
550711 return {
551712 success : true ,
0 commit comments