Compare commits
7 Commits
bugfixing_
...
fix/352
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
447332e558 | ||
|
|
9bbc19ae44 | ||
|
|
5564ae0393 | ||
|
|
93d7842f6c | ||
|
|
84c02048bc | ||
|
|
66a3bb3203 | ||
|
|
0da802be42 |
7
package-lock.json
generated
7
package-lock.json
generated
@@ -64,6 +64,7 @@
|
|||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"@vitest/coverage-v8": "^4.0.14",
|
"@vitest/coverage-v8": "^4.0.14",
|
||||||
"@vitest/ui": "^4.0.14",
|
"@vitest/ui": "^4.0.14",
|
||||||
|
"baseline-browser-mapping": "^2.8.32",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"eslint-config-next": "^16.0.5",
|
"eslint-config-next": "^16.0.5",
|
||||||
"jsdom": "^27.2.0",
|
"jsdom": "^27.2.0",
|
||||||
@@ -5203,9 +5204,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.31",
|
"version": "2.8.32",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz",
|
||||||
"integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==",
|
"integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/adapter-better-sqlite3": "^7.0.1",
|
"@prisma/adapter-better-sqlite3": "^7.0.1",
|
||||||
"@prisma/client": "^7.0.1",
|
"@prisma/client": "^7.0.1",
|
||||||
"better-sqlite3": "^12.4.6",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@t3-oss/env-nextjs": "^0.13.8",
|
"@t3-oss/env-nextjs": "^0.13.8",
|
||||||
@@ -43,6 +42,7 @@
|
|||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
"better-sqlite3": "^12.4.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cron-validator": "^1.4.0",
|
"cron-validator": "^1.4.0",
|
||||||
@@ -80,6 +80,7 @@
|
|||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"@vitest/coverage-v8": "^4.0.14",
|
"@vitest/coverage-v8": "^4.0.14",
|
||||||
"@vitest/ui": "^4.0.14",
|
"@vitest/ui": "^4.0.14",
|
||||||
|
"baseline-browser-mapping": "^2.8.32",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"eslint-config-next": "^16.0.5",
|
"eslint-config-next": "^16.0.5",
|
||||||
"jsdom": "^27.2.0",
|
"jsdom": "^27.2.0",
|
||||||
@@ -88,9 +89,9 @@
|
|||||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
"prettier-plugin-tailwindcss": "^0.7.1",
|
||||||
"prisma": "^7.0.1",
|
"prisma": "^7.0.1",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
|
"tsx": "^4.19.4",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.48.0",
|
"typescript-eslint": "^8.48.0",
|
||||||
"tsx": "^4.19.4",
|
|
||||||
"vitest": "^4.0.14"
|
"vitest": "^4.0.14"
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
@@ -103,4 +104,4 @@
|
|||||||
"overrides": {
|
"overrides": {
|
||||||
"prismjs": "^1.30.0"
|
"prismjs": "^1.30.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1630,7 +1630,7 @@ export function GeneralSettingsModal({
|
|||||||
https://github.com/owner/repo)
|
https://github.com/owner/repo)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="border-border flex items-center justify-between gap-3 rounded-lg border p-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-foreground text-sm font-medium">
|
<p className="text-foreground text-sm font-medium">
|
||||||
Enable after adding
|
Enable after adding
|
||||||
@@ -1644,6 +1644,7 @@ export function GeneralSettingsModal({
|
|||||||
onCheckedChange={setNewRepoEnabled}
|
onCheckedChange={setNewRepoEnabled}
|
||||||
disabled={isAddingRepo}
|
disabled={isAddingRepo}
|
||||||
label="Enable repository"
|
label="Enable repository"
|
||||||
|
labelPosition="left"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -1739,44 +1740,7 @@ export function GeneralSettingsModal({
|
|||||||
{repo.enabled ? "• Enabled" : "• Disabled"}
|
{repo.enabled ? "• Enabled" : "• Disabled"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
<Toggle
|
|
||||||
checked={repo.enabled}
|
|
||||||
onCheckedChange={async (enabled) => {
|
|
||||||
setMessage(null);
|
|
||||||
try {
|
|
||||||
const result =
|
|
||||||
await updateRepoMutation.mutateAsync({
|
|
||||||
id: repo.id,
|
|
||||||
enabled,
|
|
||||||
});
|
|
||||||
if (result.success) {
|
|
||||||
setMessage({
|
|
||||||
type: "success",
|
|
||||||
text: `Repository ${enabled ? "enabled" : "disabled"} successfully!`,
|
|
||||||
});
|
|
||||||
await refetchRepositories();
|
|
||||||
} else {
|
|
||||||
setMessage({
|
|
||||||
type: "error",
|
|
||||||
text:
|
|
||||||
result.error ??
|
|
||||||
"Failed to update repository",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setMessage({
|
|
||||||
type: "error",
|
|
||||||
text:
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Failed to update repository",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={updateRepoMutation.isPending}
|
|
||||||
label={repo.enabled ? "Disable" : "Enable"}
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!repo.is_removable) {
|
if (!repo.is_removable) {
|
||||||
@@ -1837,6 +1801,44 @@ export function GeneralSettingsModal({
|
|||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Toggle
|
||||||
|
checked={repo.enabled}
|
||||||
|
onCheckedChange={async (enabled) => {
|
||||||
|
setMessage(null);
|
||||||
|
try {
|
||||||
|
const result =
|
||||||
|
await updateRepoMutation.mutateAsync({
|
||||||
|
id: repo.id,
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
if (result.success) {
|
||||||
|
setMessage({
|
||||||
|
type: "success",
|
||||||
|
text: `Repository ${enabled ? "enabled" : "disabled"} successfully!`,
|
||||||
|
});
|
||||||
|
await refetchRepositories();
|
||||||
|
} else {
|
||||||
|
setMessage({
|
||||||
|
type: "error",
|
||||||
|
text:
|
||||||
|
result.error ??
|
||||||
|
"Failed to update repository",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setMessage({
|
||||||
|
type: "error",
|
||||||
|
text:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to update repository",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={updateRepoMutation.isPending}
|
||||||
|
label={repo.enabled ? "Disable" : "Enable"}
|
||||||
|
labelPosition="left"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -709,6 +709,8 @@ export function InstalledScriptsTab() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const containerType = script.is_vm ? "VM" : "LXC";
|
||||||
|
|
||||||
setConfirmationModal({
|
setConfirmationModal({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
variant: "simple",
|
variant: "simple",
|
||||||
@@ -718,7 +720,7 @@ export function InstalledScriptsTab() {
|
|||||||
setControllingScriptId(script.id);
|
setControllingScriptId(script.id);
|
||||||
setLoadingModal({
|
setLoadingModal({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
action: `${action === "start" ? "Starting" : "Stopping"} container ${script.container_id}...`,
|
action: `${action === "start" ? "Starting" : "Stopping"} ${containerType}...`,
|
||||||
});
|
});
|
||||||
void controlContainerMutation.mutate({ id: script.id, action });
|
void controlContainerMutation.mutate({ id: script.id, action });
|
||||||
setConfirmationModal(null);
|
setConfirmationModal(null);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ interface LoadingModalProps {
|
|||||||
|
|
||||||
export function LoadingModal({
|
export function LoadingModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
action: _action,
|
action,
|
||||||
logs = [],
|
logs = [],
|
||||||
isComplete = false,
|
isComplete = false,
|
||||||
title,
|
title,
|
||||||
@@ -64,6 +64,11 @@ export function LoadingModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Action text - displayed prominently */}
|
||||||
|
{action && (
|
||||||
|
<p className="text-foreground text-base font-medium">{action}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Static title text */}
|
{/* Static title text */}
|
||||||
{title && <p className="text-muted-foreground text-sm">{title}</p>}
|
{title && <p className="text-muted-foreground text-sm">{title}</p>}
|
||||||
|
|
||||||
|
|||||||
@@ -6,30 +6,40 @@ export interface ToggleProps
|
|||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
onCheckedChange?: (checked: boolean) => void;
|
onCheckedChange?: (checked: boolean) => void;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
labelPosition?: 'left' | 'right';
|
||||||
}
|
}
|
||||||
|
|
||||||
const Toggle = React.forwardRef<HTMLInputElement, ToggleProps>(
|
const Toggle = React.forwardRef<HTMLInputElement, ToggleProps>(
|
||||||
({ className, checked, onCheckedChange, label, ...props }, ref) => {
|
({ className, checked, onCheckedChange, label, labelPosition = 'right', ...props }, ref) => {
|
||||||
|
const toggleSwitch = (
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="sr-only"
|
||||||
|
checked={checked}
|
||||||
|
onChange={(e) => onCheckedChange?.(e.target.checked)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<div className={cn(
|
||||||
|
"w-11 h-6 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary/20 rounded-full peer after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 dark:after:border-gray-500 after:border after:rounded-full after:h-5 after:w-5 after:transition-transform after:duration-300 after:ease-in-out after:shadow-md transition-colors duration-300 ease-in-out border-2 border-gray-300 dark:border-gray-600",
|
||||||
|
checked
|
||||||
|
? "bg-blue-500 dark:bg-blue-600 after:translate-x-full"
|
||||||
|
: "bg-gray-300 dark:bg-gray-700",
|
||||||
|
className
|
||||||
|
)} />
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<label className="relative inline-flex items-center cursor-pointer">
|
{label && labelPosition === 'left' && (
|
||||||
<input
|
<span className="text-sm font-medium text-foreground">
|
||||||
type="checkbox"
|
{label}
|
||||||
className="sr-only"
|
</span>
|
||||||
checked={checked}
|
)}
|
||||||
onChange={(e) => onCheckedChange?.(e.target.checked)}
|
{toggleSwitch}
|
||||||
ref={ref}
|
{label && labelPosition === 'right' && (
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
<div className={cn(
|
|
||||||
"w-11 h-6 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary/20 rounded-full peer after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 dark:after:border-gray-500 after:border after:rounded-full after:h-5 after:w-5 after:transition-transform after:duration-300 after:ease-in-out after:shadow-md transition-colors duration-300 ease-in-out border-2 border-gray-300 dark:border-gray-600",
|
|
||||||
checked
|
|
||||||
? "bg-blue-500 dark:bg-blue-600 after:translate-x-full"
|
|
||||||
: "bg-gray-300 dark:bg-gray-700",
|
|
||||||
className
|
|
||||||
)} />
|
|
||||||
</label>
|
|
||||||
{label && (
|
|
||||||
<span className="text-sm font-medium text-foreground">
|
<span className="text-sm font-medium text-foreground">
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -458,14 +458,118 @@ 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) {
|
} catch (error) {
|
||||||
console.error('Error determining container type:', error);
|
console.error('Error determining container type:', error);
|
||||||
return false; // Default to LXC on error
|
return false; // Default to LXC on error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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<Map<string, boolean>> {
|
||||||
|
const containerTypeMap = new Map<string, boolean>();
|
||||||
|
|
||||||
|
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<void>((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<void>((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({
|
export const installedScriptsRouter = createTRPCRouter({
|
||||||
// Get all installed scripts
|
// Get all installed scripts
|
||||||
@@ -475,13 +579,52 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
const scripts = await db.getAllInstalledScripts();
|
const scripts = await db.getAllInstalledScripts();
|
||||||
|
|
||||||
|
// Group scripts by server_id for batch detection
|
||||||
|
const scriptsByServer = new Map<number, any[]>();
|
||||||
|
const serversMap = new Map<number, Server>();
|
||||||
|
|
||||||
|
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<string, boolean>();
|
||||||
|
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
|
// Transform scripts to flatten server data for frontend compatibility
|
||||||
|
const transformedScripts = scripts.map((script: any) => {
|
||||||
const transformedScripts = await Promise.all(scripts.map(async (script: any) => {
|
// Determine if it's a VM or LXC from batch detection map, fall back to isVM() if not found
|
||||||
// Determine if it's a VM or LXC
|
|
||||||
let is_vm = false;
|
let is_vm = false;
|
||||||
if (script.container_id && script.server_id) {
|
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 {
|
return {
|
||||||
@@ -498,7 +641,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
is_vm,
|
is_vm,
|
||||||
server: undefined // Remove nested server object
|
server: undefined // Remove nested server object
|
||||||
};
|
};
|
||||||
}));
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -522,13 +665,31 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
const scripts = await db.getInstalledScriptsByServer(input.serverId);
|
const scripts = await db.getInstalledScriptsByServer(input.serverId);
|
||||||
|
|
||||||
|
// Batch detect container types for this server
|
||||||
|
let containerTypeMap = new Map<string, boolean>();
|
||||||
|
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
|
// Transform scripts to flatten server data for frontend compatibility
|
||||||
|
const transformedScripts = scripts.map((script: any) => {
|
||||||
const transformedScripts = await Promise.all(scripts.map(async (script: any) => {
|
// Determine if it's a VM or LXC from batch detection map, fall back to LXCConfig check if not found
|
||||||
// Determine if it's a VM or LXC
|
|
||||||
let is_vm = false;
|
let is_vm = false;
|
||||||
if (script.container_id && script.server_id) {
|
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 {
|
return {
|
||||||
@@ -545,7 +706,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
is_vm,
|
is_vm,
|
||||||
server: undefined // Remove nested server object
|
server: undefined // Remove nested server object
|
||||||
};
|
};
|
||||||
}));
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -281,7 +281,8 @@ class DatabaseServicePrisma {
|
|||||||
async getAllInstalledScripts(): Promise<InstalledScriptWithServer[]> {
|
async getAllInstalledScripts(): Promise<InstalledScriptWithServer[]> {
|
||||||
const result = await prisma.installedScript.findMany({
|
const result = await prisma.installedScript.findMany({
|
||||||
include: {
|
include: {
|
||||||
server: true
|
server: true,
|
||||||
|
lxc_config: true
|
||||||
},
|
},
|
||||||
orderBy: { installation_date: 'desc' }
|
orderBy: { installation_date: 'desc' }
|
||||||
});
|
});
|
||||||
@@ -302,7 +303,8 @@ class DatabaseServicePrisma {
|
|||||||
const result = await prisma.installedScript.findMany({
|
const result = await prisma.installedScript.findMany({
|
||||||
where: { server_id },
|
where: { server_id },
|
||||||
include: {
|
include: {
|
||||||
server: true
|
server: true,
|
||||||
|
lxc_config: true
|
||||||
},
|
},
|
||||||
orderBy: { installation_date: 'desc' }
|
orderBy: { installation_date: 'desc' }
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user