From 84c02048bcdac532c72a90cfcfa13d3b5ecc7170 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Sat, 29 Nov 2025 15:41:49 +0100 Subject: [PATCH 1/3] Fix a false detection as a VM when it is a LXC --- package-lock.json | 7 ++++--- package.json | 7 ++++--- src/server/api/routers/installedScripts.ts | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index ad7f30b..a22e1ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,7 @@ "@vitejs/plugin-react": "^5.1.1", "@vitest/coverage-v8": "^4.0.14", "@vitest/ui": "^4.0.14", + "baseline-browser-mapping": "^2.8.32", "eslint": "^9.39.1", "eslint-config-next": "^16.0.5", "jsdom": "^27.2.0", @@ -5203,9 +5204,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.31", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz", - "integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==", + "version": "2.8.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz", + "integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 79d5a6e..aa4a159 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "dependencies": { "@prisma/adapter-better-sqlite3": "^7.0.1", "@prisma/client": "^7.0.1", - "better-sqlite3": "^12.4.6", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.4", "@t3-oss/env-nextjs": "^0.13.8", @@ -43,6 +42,7 @@ "@xterm/xterm": "^5.5.0", "axios": "^1.13.2", "bcryptjs": "^3.0.3", + "better-sqlite3": "^12.4.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cron-validator": "^1.4.0", @@ -80,6 +80,7 @@ "@vitejs/plugin-react": "^5.1.1", "@vitest/coverage-v8": "^4.0.14", "@vitest/ui": "^4.0.14", + "baseline-browser-mapping": "^2.8.32", "eslint": "^9.39.1", "eslint-config-next": "^16.0.5", "jsdom": "^27.2.0", @@ -88,9 +89,9 @@ "prettier-plugin-tailwindcss": "^0.7.1", "prisma": "^7.0.1", "tailwindcss": "^4.1.17", + "tsx": "^4.19.4", "typescript": "^5.9.3", "typescript-eslint": "^8.48.0", - "tsx": "^4.19.4", "vitest": "^4.0.14" }, "ct3aMetadata": { @@ -103,4 +104,4 @@ "overrides": { "prismjs": "^1.30.0" } -} \ No newline at end of file +} diff --git a/src/server/api/routers/installedScripts.ts b/src/server/api/routers/installedScripts.ts index 96c516f..dde8a08 100644 --- a/src/server/api/routers/installedScripts.ts +++ b/src/server/api/routers/installedScripts.ts @@ -458,8 +458,8 @@ async function isVM(scriptId: number, containerId: string, serverId: number | nu ); }); - // If LXC config exists, it's an LXC container - return !lxcConfigExists; // Return true if it's a VM (neither config exists defaults to false/LXC) + + return false; // Always LXC since VM config doesn't exist } catch (error) { console.error('Error determining container type:', error); return false; // Default to LXC on error From 93d7842f6ca2879cbe83fd82290c7d4f5a5c4447 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Sat, 29 Nov 2025 15:55:43 +0100 Subject: [PATCH 2/3] feat: implement batch container type detection for performance optimization - Add batchDetectContainerTypes() helper function that uses pct list and qm list to detect all container types in 2 SSH calls per server - Update getAllInstalledScripts to use batch detection instead of individual isVM() calls per script - Update getInstalledScriptsByServer to use batch detection for single server - Update database queries to include lxc_config relation for fallback detection - Fix isVM() function to properly default to LXC when VM config doesn't exist - Significantly improves performance: reduces from N SSH calls per script to 2 SSH calls per server --- src/server/api/routers/installedScripts.ts | 181 +++++++++++++++++++-- src/server/database-prisma.ts | 6 +- 2 files changed, 175 insertions(+), 12 deletions(-) diff --git a/src/server/api/routers/installedScripts.ts b/src/server/api/routers/installedScripts.ts index dde8a08..02f416e 100644 --- a/src/server/api/routers/installedScripts.ts +++ b/src/server/api/routers/installedScripts.ts @@ -466,6 +466,110 @@ async function isVM(scriptId: number, containerId: string, serverId: number | nu } } +// Helper function to batch detect container types for all containers on a server +// Returns a Map of container_id -> isVM (true for VM, false for LXC) +async function batchDetectContainerTypes(server: Server): Promise> { + const containerTypeMap = new Map(); + + try { + // Import SSH services + const { default: SSHService } = await import('~/server/ssh-service'); + const { default: SSHExecutionService } = await import('~/server/ssh-execution-service'); + const sshService = new SSHService(); + const sshExecutionService = new SSHExecutionService(); + + // Test SSH connection first + const connectionTest = await sshService.testSSHConnection(server); + if (!(connectionTest as any).success) { + console.warn(`SSH connection failed for server ${server.name}, skipping batch detection`); + return containerTypeMap; // Return empty map if SSH fails + } + + // Helper function to parse list output and extract IDs + const parseListOutput = (output: string): string[] => { + const ids: string[] = []; + const lines = output.split('\n').filter(line => line.trim()); + + for (const line of lines) { + // Skip header lines + if (line.includes('VMID') || line.includes('CTID')) continue; + + // Extract first column (ID) + const parts = line.trim().split(/\s+/); + if (parts.length > 0) { + const id = parts[0]?.trim(); + // Validate ID format (3-4 digits typically) + if (id && /^\d{3,4}$/.test(id)) { + ids.push(id); + } + } + } + + return ids; + }; + + // Get containers from pct list + let pctOutput = ''; + await new Promise((resolve, reject) => { + void sshExecutionService.executeCommand( + server, + 'pct list', + (data: string) => { + pctOutput += data; + }, + (error: string) => { + console.error(`pct list error for server ${server.name}:`, error); + // Don't reject, just continue - might be no containers + resolve(); + }, + (_exitCode: number) => { + resolve(); + } + ); + }); + + // Get VMs from qm list + let qmOutput = ''; + await new Promise((resolve, reject) => { + void sshExecutionService.executeCommand( + server, + 'qm list', + (data: string) => { + qmOutput += data; + }, + (error: string) => { + console.error(`qm list error for server ${server.name}:`, error); + // Don't reject, just continue - might be no VMs + resolve(); + }, + (_exitCode: number) => { + resolve(); + } + ); + }); + + // Parse IDs from both lists + const containerIds = parseListOutput(pctOutput); + const vmIds = parseListOutput(qmOutput); + + // Mark all LXC containers as false (not VM) + for (const id of containerIds) { + containerTypeMap.set(id, false); + } + + // Mark all VMs as true (is VM) + for (const id of vmIds) { + containerTypeMap.set(id, true); + } + + } catch (error) { + console.error(`Error in batchDetectContainerTypes for server ${server.name}:`, error); + // Return empty map on error - individual checks will fall back to isVM() + } + + return containerTypeMap; +} + export const installedScriptsRouter = createTRPCRouter({ // Get all installed scripts @@ -475,13 +579,52 @@ export const installedScriptsRouter = createTRPCRouter({ const db = getDatabase(); const scripts = await db.getAllInstalledScripts(); + // Group scripts by server_id for batch detection + const scriptsByServer = new Map(); + const serversMap = new Map(); + + for (const script of scripts) { + if (script.server_id && script.server) { + if (!scriptsByServer.has(script.server_id)) { + scriptsByServer.set(script.server_id, []); + serversMap.set(script.server_id, script.server as Server); + } + scriptsByServer.get(script.server_id)!.push(script); + } + } + + // Batch detect container types for each server + const containerTypeMap = new Map(); + const batchDetectionPromises = Array.from(serversMap.entries()).map(async ([serverId, server]) => { + try { + const serverTypeMap = await batchDetectContainerTypes(server); + // Merge into main map with server-specific prefix to avoid collisions + // Actually, container IDs are unique across the cluster, so we can use them directly + for (const [containerId, isVM] of serverTypeMap.entries()) { + containerTypeMap.set(containerId, isVM); + } + } catch (error) { + console.error(`Error batch detecting types for server ${serverId}:`, error); + // Continue with other servers + } + }); + + await Promise.all(batchDetectionPromises); + // Transform scripts to flatten server data for frontend compatibility - - const transformedScripts = await Promise.all(scripts.map(async (script: any) => { - // Determine if it's a VM or LXC + const transformedScripts = scripts.map((script: any) => { + // Determine if it's a VM or LXC from batch detection map, fall back to isVM() if not found let is_vm = false; if (script.container_id && script.server_id) { - is_vm = await isVM(script.id, script.container_id, script.server_id); + // First check if we have it in the batch detection map + if (containerTypeMap.has(script.container_id)) { + is_vm = containerTypeMap.get(script.container_id) ?? false; + } else { + // Fall back to checking LXCConfig in database (fast, no SSH needed) + // If LXCConfig exists, it's an LXC container + const hasLXCConfig = script.lxc_config !== null && script.lxc_config !== undefined; + is_vm = !hasLXCConfig; // If no LXCConfig, might be VM, but default to false for safety + } } return { @@ -498,7 +641,7 @@ export const installedScriptsRouter = createTRPCRouter({ is_vm, server: undefined // Remove nested server object }; - })); + }); return { success: true, @@ -522,13 +665,31 @@ export const installedScriptsRouter = createTRPCRouter({ const db = getDatabase(); const scripts = await db.getInstalledScriptsByServer(input.serverId); + // Batch detect container types for this server + let containerTypeMap = new Map(); + if (scripts.length > 0 && scripts[0]?.server) { + try { + containerTypeMap = await batchDetectContainerTypes(scripts[0].server as Server); + } catch (error) { + console.error(`Error batch detecting types for server ${input.serverId}:`, error); + // Continue with empty map, will fall back to LXCConfig check + } + } + // Transform scripts to flatten server data for frontend compatibility - - const transformedScripts = await Promise.all(scripts.map(async (script: any) => { - // Determine if it's a VM or LXC + const transformedScripts = scripts.map((script: any) => { + // Determine if it's a VM or LXC from batch detection map, fall back to LXCConfig check if not found let is_vm = false; if (script.container_id && script.server_id) { - is_vm = await isVM(script.id, script.container_id, script.server_id); + // First check if we have it in the batch detection map + if (containerTypeMap.has(script.container_id)) { + is_vm = containerTypeMap.get(script.container_id) ?? false; + } else { + // Fall back to checking LXCConfig in database (fast, no SSH needed) + // If LXCConfig exists, it's an LXC container + const hasLXCConfig = script.lxc_config !== null && script.lxc_config !== undefined; + is_vm = !hasLXCConfig; // If no LXCConfig, might be VM, but default to false for safety + } } return { @@ -545,7 +706,7 @@ export const installedScriptsRouter = createTRPCRouter({ is_vm, server: undefined // Remove nested server object }; - })); + }); return { success: true, diff --git a/src/server/database-prisma.ts b/src/server/database-prisma.ts index b7de9ac..8eb8781 100644 --- a/src/server/database-prisma.ts +++ b/src/server/database-prisma.ts @@ -281,7 +281,8 @@ class DatabaseServicePrisma { async getAllInstalledScripts(): Promise { const result = await prisma.installedScript.findMany({ include: { - server: true + server: true, + lxc_config: true }, orderBy: { installation_date: 'desc' } }); @@ -302,7 +303,8 @@ class DatabaseServicePrisma { const result = await prisma.installedScript.findMany({ where: { server_id }, include: { - server: true + server: true, + lxc_config: true }, orderBy: { installation_date: 'desc' } }); From 5564ae0393590c0c04dabedbf3fcc45fc5b9a124 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Sat, 29 Nov 2025 15:58:30 +0100 Subject: [PATCH 3/3] fix: add dynamic text to container control loading modal - Update LoadingModal to display action text (Starting/Stopping LXC/VM) - Update handleStartStop to include container type (LXC/VM) in action text - Show clear feedback when starting or stopping containers --- src/app/_components/InstalledScriptsTab.tsx | 4 +++- src/app/_components/LoadingModal.tsx | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/app/_components/InstalledScriptsTab.tsx b/src/app/_components/InstalledScriptsTab.tsx index 4799a31..a10e8cb 100644 --- a/src/app/_components/InstalledScriptsTab.tsx +++ b/src/app/_components/InstalledScriptsTab.tsx @@ -709,6 +709,8 @@ export function InstalledScriptsTab() { return; } + const containerType = script.is_vm ? "VM" : "LXC"; + setConfirmationModal({ isOpen: true, variant: "simple", @@ -718,7 +720,7 @@ export function InstalledScriptsTab() { setControllingScriptId(script.id); setLoadingModal({ isOpen: true, - action: `${action === "start" ? "Starting" : "Stopping"} container ${script.container_id}...`, + action: `${action === "start" ? "Starting" : "Stopping"} ${containerType}...`, }); void controlContainerMutation.mutate({ id: script.id, action }); setConfirmationModal(null); diff --git a/src/app/_components/LoadingModal.tsx b/src/app/_components/LoadingModal.tsx index 9a06d3c..f3c642e 100644 --- a/src/app/_components/LoadingModal.tsx +++ b/src/app/_components/LoadingModal.tsx @@ -16,7 +16,7 @@ interface LoadingModalProps { export function LoadingModal({ isOpen, - action: _action, + action, logs = [], isComplete = false, title, @@ -64,6 +64,11 @@ export function LoadingModal({ )} + {/* Action text - displayed prominently */} + {action && ( +

{action}

+ )} + {/* Static title text */} {title &&

{title}

}