Skip to content

Commit 9bbc19a

Browse files
Merge pull request #358 from community-scripts/fix/357_356
fix: Add dynamic text to container control loading modal
2 parents 66a3bb3 + 5564ae0 commit 9bbc19a

File tree

6 files changed

+194
-22
lines changed

6 files changed

+194
-22
lines changed

package-lock.json

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
"dependencies": {
2828
"@prisma/adapter-better-sqlite3": "^7.0.1",
2929
"@prisma/client": "^7.0.1",
30-
"better-sqlite3": "^12.4.6",
3130
"@radix-ui/react-dropdown-menu": "^2.1.16",
3231
"@radix-ui/react-slot": "^1.2.4",
3332
"@t3-oss/env-nextjs": "^0.13.8",
@@ -43,6 +42,7 @@
4342
"@xterm/xterm": "^5.5.0",
4443
"axios": "^1.13.2",
4544
"bcryptjs": "^3.0.3",
45+
"better-sqlite3": "^12.4.6",
4646
"class-variance-authority": "^0.7.1",
4747
"clsx": "^2.1.1",
4848
"cron-validator": "^1.4.0",
@@ -80,6 +80,7 @@
8080
"@vitejs/plugin-react": "^5.1.1",
8181
"@vitest/coverage-v8": "^4.0.14",
8282
"@vitest/ui": "^4.0.14",
83+
"baseline-browser-mapping": "^2.8.32",
8384
"eslint": "^9.39.1",
8485
"eslint-config-next": "^16.0.5",
8586
"jsdom": "^27.2.0",
@@ -88,9 +89,9 @@
8889
"prettier-plugin-tailwindcss": "^0.7.1",
8990
"prisma": "^7.0.1",
9091
"tailwindcss": "^4.1.17",
92+
"tsx": "^4.19.4",
9193
"typescript": "^5.9.3",
9294
"typescript-eslint": "^8.48.0",
93-
"tsx": "^4.19.4",
9495
"vitest": "^4.0.14"
9596
},
9697
"ct3aMetadata": {
@@ -103,4 +104,4 @@
103104
"overrides": {
104105
"prismjs": "^1.30.0"
105106
}
106-
}
107+
}

src/app/_components/InstalledScriptsTab.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,8 @@ export function InstalledScriptsTab() {
709709
return;
710710
}
711711

712+
const containerType = script.is_vm ? "VM" : "LXC";
713+
712714
setConfirmationModal({
713715
isOpen: true,
714716
variant: "simple",
@@ -718,7 +720,7 @@ export function InstalledScriptsTab() {
718720
setControllingScriptId(script.id);
719721
setLoadingModal({
720722
isOpen: true,
721-
action: `${action === "start" ? "Starting" : "Stopping"} container ${script.container_id}...`,
723+
action: `${action === "start" ? "Starting" : "Stopping"} ${containerType}...`,
722724
});
723725
void controlContainerMutation.mutate({ id: script.id, action });
724726
setConfirmationModal(null);

src/app/_components/LoadingModal.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ interface LoadingModalProps {
1616

1717
export function LoadingModal({
1818
isOpen,
19-
action: _action,
19+
action,
2020
logs = [],
2121
isComplete = false,
2222
title,
@@ -64,6 +64,11 @@ export function LoadingModal({
6464
)}
6565
</div>
6666

67+
{/* Action text - displayed prominently */}
68+
{action && (
69+
<p className="text-foreground text-base font-medium">{action}</p>
70+
)}
71+
6772
{/* Static title text */}
6873
{title && <p className="text-muted-foreground text-sm">{title}</p>}
6974

src/server/api/routers/installedScripts.ts

Lines changed: 173 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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

470574
export 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,

src/server/database-prisma.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,8 @@ class DatabaseServicePrisma {
281281
async getAllInstalledScripts(): Promise<InstalledScriptWithServer[]> {
282282
const result = await prisma.installedScript.findMany({
283283
include: {
284-
server: true
284+
server: true,
285+
lxc_config: true
285286
},
286287
orderBy: { installation_date: 'desc' }
287288
});
@@ -302,7 +303,8 @@ class DatabaseServicePrisma {
302303
const result = await prisma.installedScript.findMany({
303304
where: { server_id },
304305
include: {
305-
server: true
306+
server: true,
307+
lxc_config: true
306308
},
307309
orderBy: { installation_date: 'desc' }
308310
});

0 commit comments

Comments
 (0)