mirror of
https://github.com/community-scripts/ProxmoxVE-Local.git
synced 2026-07-30 09:24:05 -04:00
* feat: add container running status indicators with auto-refresh - Add getContainerStatuses tRPC endpoint for checking container status - Support both local and SSH remote container status checking - Add 60-second auto-refresh for real-time status updates - Display green/red dots next to container IDs showing running/stopped status - Update both desktop table and mobile card views - Add proper error handling and fallback to 'unknown' status - Full TypeScript support with updated interfaces Resolves container status visibility in installed scripts tab * feat: update default sorting to group by server then container ID - Change default sort field from 'script_name' to 'server_name' - Implement multi-level sorting: server name first, then container ID - Use numeric sorting for container IDs for proper ordering - Maintain existing sorting behavior for other fields - Improves organization by grouping related containers together * feat: improve container status check triggering - Add multiple triggers for container status checks: - When component mounts (tab becomes active) - When scripts data loads - Every 60 seconds via interval - Add manual 'Refresh Container Status' button for on-demand checking - Add console logging for debugging status check triggers - Ensure status checks happen reliably when switching to installed scripts tab Fixes issue where status checks weren't triggering when tab loads * perf: optimize container status checking by batching per server - Group containers by server and make one pct list call per server - Replace individual container checks with batch processing - Parse all container statuses from single pct list response per server - Add proper TypeScript safety checks for undefined values - Significantly reduce SSH calls from N containers to 1 call per server This should dramatically speed up status loading for multiple containers on the same server * fix: resolve all linting errors - Fix React Hook dependency warnings by using useCallback and useMemo - Fix TypeScript unsafe argument errors with proper Server type - Fix nullish coalescing operator preferences - Fix floating promise warnings with void operator - All ESLint warnings and errors now resolved Ensures clean code quality for PR merge
728 lines
26 KiB
TypeScript
728 lines
26 KiB
TypeScript
import { z } from "zod";
|
|
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
|
import { getDatabase } from "~/server/database";
|
|
import { exec } from "child_process";
|
|
import { promisify } from "util";
|
|
import { getSSHExecutionService } from "~/server/ssh-execution-service";
|
|
import type { Server } from "~/types/server";
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
// Helper function to check local container statuses
|
|
async function getLocalContainerStatuses(containerIds: string[]): Promise<Record<string, 'running' | 'stopped' | 'unknown'>> {
|
|
try {
|
|
const { stdout } = await execAsync('pct list');
|
|
const statusMap: Record<string, 'running' | 'stopped' | 'unknown'> = {};
|
|
|
|
// Parse pct list output
|
|
const lines = stdout.trim().split('\n');
|
|
const dataLines = lines.slice(1); // Skip header
|
|
|
|
for (const line of dataLines) {
|
|
const parts = line.trim().split(/\s+/);
|
|
if (parts.length >= 2) {
|
|
const vmid = parts[0];
|
|
const status = parts[1];
|
|
|
|
if (vmid && containerIds.includes(vmid)) {
|
|
statusMap[vmid] = status === 'running' ? 'running' : 'stopped';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set unknown for containers not found in pct list
|
|
for (const containerId of containerIds) {
|
|
if (!(containerId in statusMap)) {
|
|
statusMap[containerId] = 'unknown';
|
|
}
|
|
}
|
|
|
|
return statusMap;
|
|
} catch (error) {
|
|
console.error('Error checking local container statuses:', error);
|
|
// Return unknown for all containers on error
|
|
const statusMap: Record<string, 'running' | 'stopped' | 'unknown'> = {};
|
|
for (const containerId of containerIds) {
|
|
statusMap[containerId] = 'unknown';
|
|
}
|
|
return statusMap;
|
|
}
|
|
}
|
|
|
|
// Helper function to check remote container statuses (multiple containers per server)
|
|
async function getRemoteContainerStatuses(containerIds: string[], server: Server): Promise<Record<string, 'running' | 'stopped' | 'unknown'>> {
|
|
return new Promise((resolve) => {
|
|
const sshService = getSSHExecutionService();
|
|
const statusMap: Record<string, 'running' | 'stopped' | 'unknown'> = {};
|
|
|
|
// Initialize all containers as unknown
|
|
for (const containerId of containerIds) {
|
|
statusMap[containerId] = 'unknown';
|
|
}
|
|
|
|
void sshService.executeCommand(
|
|
server,
|
|
'pct list',
|
|
(data: string) => {
|
|
// Parse the output to find all containers
|
|
const lines = data.trim().split('\n');
|
|
const dataLines = lines.slice(1); // Skip header
|
|
|
|
for (const line of dataLines) {
|
|
const parts = line.trim().split(/\s+/);
|
|
if (parts.length >= 2) {
|
|
const vmid = parts[0];
|
|
const status = parts[1];
|
|
|
|
// Check if this is one of the containers we're looking for
|
|
if (vmid && containerIds.includes(vmid)) {
|
|
statusMap[vmid] = status === 'running' ? 'running' : 'stopped';
|
|
}
|
|
}
|
|
}
|
|
|
|
resolve(statusMap);
|
|
},
|
|
(error: string) => {
|
|
console.error(`Error checking remote containers on server ${server.name}:`, error);
|
|
resolve(statusMap); // Return the map with unknown statuses
|
|
},
|
|
(exitCode: number) => {
|
|
if (exitCode !== 0) {
|
|
resolve(statusMap); // Return the map with unknown statuses
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
export const installedScriptsRouter = createTRPCRouter({
|
|
// Get all installed scripts
|
|
getAllInstalledScripts: publicProcedure
|
|
.query(async () => {
|
|
try {
|
|
const db = getDatabase();
|
|
const scripts = db.getAllInstalledScripts();
|
|
return {
|
|
success: true,
|
|
scripts
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in getAllInstalledScripts:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to fetch installed scripts',
|
|
scripts: []
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Get installed scripts by server
|
|
getInstalledScriptsByServer: publicProcedure
|
|
.input(z.object({ serverId: z.number() }))
|
|
.query(async ({ input }) => {
|
|
try {
|
|
const db = getDatabase();
|
|
const scripts = db.getInstalledScriptsByServer(input.serverId);
|
|
return {
|
|
success: true,
|
|
scripts
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in getInstalledScriptsByServer:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to fetch installed scripts by server',
|
|
scripts: []
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Get installed script by ID
|
|
getInstalledScriptById: publicProcedure
|
|
.input(z.object({ id: z.number() }))
|
|
.query(async ({ input }) => {
|
|
try {
|
|
const db = getDatabase();
|
|
const script = db.getInstalledScriptById(input.id);
|
|
if (!script) {
|
|
return {
|
|
success: false,
|
|
error: 'Installed script not found',
|
|
script: null
|
|
};
|
|
}
|
|
return {
|
|
success: true,
|
|
script
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in getInstalledScriptById:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to fetch installed script',
|
|
script: null
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Create new installed script record
|
|
createInstalledScript: publicProcedure
|
|
.input(z.object({
|
|
script_name: z.string(),
|
|
script_path: z.string(),
|
|
container_id: z.string().optional(),
|
|
server_id: z.number().optional(),
|
|
execution_mode: z.enum(['local', 'ssh']),
|
|
status: z.enum(['in_progress', 'success', 'failed']),
|
|
output_log: z.string().optional()
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
try {
|
|
const db = getDatabase();
|
|
const result = db.createInstalledScript(input);
|
|
return {
|
|
success: true,
|
|
id: result.lastInsertRowid,
|
|
message: 'Installed script record created successfully'
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in createInstalledScript:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to create installed script record'
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Update installed script
|
|
updateInstalledScript: publicProcedure
|
|
.input(z.object({
|
|
id: z.number(),
|
|
script_name: z.string().optional(),
|
|
container_id: z.string().optional(),
|
|
status: z.enum(['in_progress', 'success', 'failed']).optional(),
|
|
output_log: z.string().optional()
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
try {
|
|
const { id, ...updateData } = input;
|
|
const db = getDatabase();
|
|
const result = db.updateInstalledScript(id, updateData);
|
|
|
|
if (result.changes === 0) {
|
|
return {
|
|
success: false,
|
|
error: 'No changes made or script not found'
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
message: 'Installed script updated successfully'
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in updateInstalledScript:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to update installed script'
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Delete installed script
|
|
deleteInstalledScript: publicProcedure
|
|
.input(z.object({ id: z.number() }))
|
|
.mutation(async ({ input }) => {
|
|
try {
|
|
const db = getDatabase();
|
|
const result = db.deleteInstalledScript(input.id);
|
|
|
|
if (result.changes === 0) {
|
|
return {
|
|
success: false,
|
|
error: 'Script not found or already deleted'
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
message: 'Installed script deleted successfully'
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in deleteInstalledScript:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to delete installed script'
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Get installation statistics
|
|
getInstallationStats: publicProcedure
|
|
.query(async () => {
|
|
try {
|
|
const db = getDatabase();
|
|
const allScripts = db.getAllInstalledScripts();
|
|
|
|
const stats = {
|
|
total: allScripts.length,
|
|
byStatus: {
|
|
success: allScripts.filter((s: any) => s.status === 'success').length,
|
|
failed: allScripts.filter((s: any) => s.status === 'failed').length,
|
|
in_progress: allScripts.filter((s: any) => s.status === 'in_progress').length
|
|
},
|
|
byMode: {
|
|
local: allScripts.filter((s: any) => s.execution_mode === 'local').length,
|
|
ssh: allScripts.filter((s: any) => s.execution_mode === 'ssh').length
|
|
},
|
|
byServer: {} as Record<string, number>
|
|
};
|
|
|
|
// Count by server
|
|
allScripts.forEach((script: any) => {
|
|
const serverKey = script.server_name ?? 'Local';
|
|
stats.byServer[serverKey] = (stats.byServer[serverKey] ?? 0) + 1;
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
stats
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in getInstallationStats:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to fetch installation statistics',
|
|
stats: null
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Auto-detect LXC containers with community-script tag
|
|
autoDetectLXCContainers: publicProcedure
|
|
.input(z.object({ serverId: z.number() }))
|
|
.mutation(async ({ input }) => {
|
|
console.log('=== AUTO-DETECT API ENDPOINT CALLED ===');
|
|
console.log('Input received:', input);
|
|
console.log('Timestamp:', new Date().toISOString());
|
|
|
|
try {
|
|
console.log('Starting auto-detect LXC containers for server ID:', input.serverId);
|
|
|
|
const db = getDatabase();
|
|
const server = db.getServerById(input.serverId);
|
|
|
|
if (!server) {
|
|
console.error('Server not found for ID:', input.serverId);
|
|
return {
|
|
success: false,
|
|
error: 'Server not found',
|
|
detectedContainers: []
|
|
};
|
|
}
|
|
|
|
console.log('Found server:', (server as any).name, 'at', (server as any).ip);
|
|
|
|
// 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
|
|
console.log('Testing SSH connection...');
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
const connectionTest = await sshService.testSSHConnection(server as any);
|
|
console.log('SSH connection test result:', connectionTest);
|
|
|
|
if (!(connectionTest as any).success) {
|
|
return {
|
|
success: false,
|
|
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`,
|
|
detectedContainers: []
|
|
};
|
|
}
|
|
|
|
console.log('SSH connection successful, scanning for LXC containers...');
|
|
|
|
// Use the working approach - manual loop through all config files
|
|
const command = `for file in /etc/pve/lxc/*.conf; do if [ -f "$file" ]; then if grep -q "community-script" "$file"; then echo "$file"; fi; fi; done`;
|
|
let detectedContainers: any[] = [];
|
|
|
|
console.log('Executing manual loop command...');
|
|
console.log('Command:', command);
|
|
|
|
let commandOutput = '';
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
|
|
void sshExecutionService.executeCommand(
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
server as any,
|
|
command,
|
|
(data: string) => {
|
|
commandOutput += data;
|
|
},
|
|
(error: string) => {
|
|
console.error('Command error:', error);
|
|
},
|
|
(exitCode: number) => {
|
|
console.log('Command exit code:', exitCode);
|
|
|
|
// Parse the complete output to get config file paths that contain community-script tag
|
|
const configFiles = commandOutput.split('\n')
|
|
.filter((line: string) => line.trim())
|
|
.map((line: string) => line.trim())
|
|
.filter((line: string) => line.endsWith('.conf'));
|
|
|
|
console.log('Found config files with community-script tag:', configFiles.length);
|
|
console.log('Config files:', configFiles);
|
|
|
|
// Process each config file to extract hostname
|
|
const processPromises = configFiles.map(async (configPath: string) => {
|
|
try {
|
|
const containerId = configPath.split('/').pop()?.replace('.conf', '');
|
|
if (!containerId) return null;
|
|
|
|
console.log('Processing container:', containerId, 'from', configPath);
|
|
|
|
// Read the config file content
|
|
const readCommand = `cat "${configPath}" 2>/dev/null`;
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
return new Promise<any>((readResolve) => {
|
|
|
|
void sshExecutionService.executeCommand(
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
server as any,
|
|
readCommand,
|
|
(configData: string) => {
|
|
// Parse config file for hostname
|
|
const lines = configData.split('\n');
|
|
let hostname = '';
|
|
|
|
for (const line of lines) {
|
|
const trimmedLine = line.trim();
|
|
if (trimmedLine.startsWith('hostname:')) {
|
|
hostname = trimmedLine.substring(9).trim();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hostname) {
|
|
const container = {
|
|
containerId,
|
|
hostname,
|
|
configPath,
|
|
serverId: (server as any).id,
|
|
serverName: (server as any).name
|
|
};
|
|
console.log('Adding container to detected list:', container);
|
|
readResolve(container);
|
|
} else {
|
|
console.log('No hostname found for', containerId);
|
|
readResolve(null);
|
|
}
|
|
},
|
|
(readError: string) => {
|
|
console.error(`Error reading config file ${configPath}:`, readError);
|
|
readResolve(null);
|
|
},
|
|
(_exitCode: number) => {
|
|
readResolve(null);
|
|
}
|
|
);
|
|
});
|
|
} catch (error) {
|
|
console.error(`Error processing config file ${configPath}:`, error);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// Wait for all config files to be processed
|
|
void Promise.all(processPromises).then((results) => {
|
|
detectedContainers = results.filter(result => result !== null);
|
|
console.log('Final detected containers:', detectedContainers.length);
|
|
resolve();
|
|
}).catch((error) => {
|
|
console.error('Error processing config files:', error);
|
|
reject(new Error(`Error processing config files: ${error}`));
|
|
});
|
|
}
|
|
);
|
|
});
|
|
|
|
console.log('Detected containers:', detectedContainers.length);
|
|
|
|
// Get existing scripts to check for duplicates
|
|
const existingScripts = db.getAllInstalledScripts();
|
|
|
|
// Create installed script records for detected containers (skip duplicates)
|
|
const createdScripts = [];
|
|
const skippedScripts = [];
|
|
|
|
for (const container of detectedContainers) {
|
|
try {
|
|
// Check if a script with this container_id and server_id already exists
|
|
const duplicate = existingScripts.find((script: any) =>
|
|
script.container_id === container.containerId &&
|
|
script.server_id === container.serverId
|
|
);
|
|
|
|
if (duplicate) {
|
|
console.log(`Skipping duplicate: ${container.hostname} (${container.containerId}) already exists`);
|
|
skippedScripts.push({
|
|
containerId: container.containerId,
|
|
hostname: container.hostname,
|
|
serverName: container.serverName
|
|
});
|
|
continue;
|
|
}
|
|
|
|
console.log('Creating script record for:', container.hostname, container.containerId);
|
|
const result = db.createInstalledScript({
|
|
script_name: container.hostname,
|
|
script_path: `detected/${container.hostname}`,
|
|
container_id: container.containerId,
|
|
server_id: container.serverId,
|
|
execution_mode: 'ssh',
|
|
status: 'success',
|
|
output_log: `Auto-detected from LXC config: ${container.configPath}`
|
|
});
|
|
|
|
createdScripts.push({
|
|
id: result.lastInsertRowid,
|
|
containerId: container.containerId,
|
|
hostname: container.hostname,
|
|
serverName: container.serverName
|
|
});
|
|
console.log('Created script record with ID:', result.lastInsertRowid);
|
|
} catch (error) {
|
|
console.error(`Error creating script record for ${container.hostname}:`, error);
|
|
}
|
|
}
|
|
|
|
const message = skippedScripts.length > 0
|
|
? `Auto-detection completed. Found ${detectedContainers.length} containers with community-script tag. Added ${createdScripts.length} new scripts, skipped ${skippedScripts.length} duplicates.`
|
|
: `Auto-detection completed. Found ${detectedContainers.length} containers with community-script tag. Added ${createdScripts.length} new scripts.`;
|
|
|
|
return {
|
|
success: true,
|
|
message: message,
|
|
detectedContainers: createdScripts,
|
|
skippedContainers: skippedScripts
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in autoDetectLXCContainers:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to auto-detect LXC containers',
|
|
detectedContainers: []
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Cleanup orphaned scripts (check if LXC containers still exist on servers)
|
|
cleanupOrphanedScripts: publicProcedure
|
|
.mutation(async () => {
|
|
try {
|
|
console.log('=== CLEANUP ORPHANED SCRIPTS API ENDPOINT CALLED ===');
|
|
console.log('Timestamp:', new Date().toISOString());
|
|
|
|
const db = getDatabase();
|
|
const allScripts = db.getAllInstalledScripts();
|
|
const allServers = db.getAllServers();
|
|
|
|
console.log('Found scripts:', allScripts.length);
|
|
console.log('Found servers:', allServers.length);
|
|
|
|
if (allScripts.length === 0) {
|
|
return {
|
|
success: true,
|
|
message: 'No scripts to check',
|
|
deletedCount: 0,
|
|
deletedScripts: []
|
|
};
|
|
}
|
|
|
|
// 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();
|
|
|
|
const deletedScripts: string[] = [];
|
|
const scriptsToCheck = allScripts.filter((script: any) =>
|
|
script.execution_mode === 'ssh' &&
|
|
script.server_id &&
|
|
script.container_id
|
|
);
|
|
|
|
console.log('Scripts to check for cleanup:', scriptsToCheck.length);
|
|
|
|
for (const script of scriptsToCheck) {
|
|
try {
|
|
const scriptData = script as any;
|
|
const server = allServers.find((s: any) => s.id === scriptData.server_id);
|
|
if (!server) {
|
|
console.log(`Server not found for script ${scriptData.script_name}, marking for deletion`);
|
|
db.deleteInstalledScript(Number(scriptData.id));
|
|
deletedScripts.push(String(scriptData.script_name));
|
|
continue;
|
|
}
|
|
|
|
console.log(`Checking script ${scriptData.script_name} on server ${(server as any).name}`);
|
|
|
|
// Test SSH connection
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
const connectionTest = await sshService.testSSHConnection(server as any);
|
|
if (!(connectionTest as any).success) {
|
|
console.log(`SSH connection failed for server ${(server as any).name}, skipping script ${scriptData.script_name}`);
|
|
continue;
|
|
}
|
|
|
|
// Check if the container config file still exists
|
|
const checkCommand = `test -f "/etc/pve/lxc/${scriptData.container_id}.conf" && echo "exists" || echo "not_found"`;
|
|
|
|
const containerExists = await new Promise<boolean>((resolve) => {
|
|
|
|
void sshExecutionService.executeCommand(
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
server as any,
|
|
checkCommand,
|
|
(data: string) => {
|
|
resolve(data.trim() === 'exists');
|
|
},
|
|
(error: string) => {
|
|
console.error(`Error checking container ${scriptData.script_name}:`, error);
|
|
resolve(false);
|
|
},
|
|
(_exitCode: number) => {
|
|
resolve(false);
|
|
}
|
|
);
|
|
});
|
|
|
|
if (!containerExists) {
|
|
console.log(`Container ${scriptData.container_id} not found on server ${(server as any).name}, deleting script ${scriptData.script_name}`);
|
|
db.deleteInstalledScript(Number(scriptData.id));
|
|
deletedScripts.push(String(scriptData.script_name));
|
|
} else {
|
|
console.log(`Container ${scriptData.container_id} still exists on server ${(server as any).name}, keeping script ${scriptData.script_name}`);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`Error checking script ${(script as any).script_name}:`, error);
|
|
}
|
|
}
|
|
|
|
console.log('Cleanup completed. Deleted scripts:', deletedScripts);
|
|
|
|
return {
|
|
success: true,
|
|
message: `Cleanup completed. ${deletedScripts.length} orphaned script(s) removed.`,
|
|
deletedCount: deletedScripts.length,
|
|
deletedScripts: deletedScripts
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in cleanupOrphanedScripts:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to cleanup orphaned scripts',
|
|
deletedCount: 0,
|
|
deletedScripts: []
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Get container running statuses
|
|
getContainerStatuses: publicProcedure
|
|
.input(z.object({
|
|
containers: z.array(z.object({
|
|
containerId: z.string(),
|
|
serverId: z.number().optional(),
|
|
server: z.object({
|
|
id: z.number(),
|
|
name: z.string(),
|
|
ip: z.string(),
|
|
user: z.string(),
|
|
password: z.string(),
|
|
auth_type: z.string()
|
|
}).optional()
|
|
}))
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
try {
|
|
const { containers } = input;
|
|
const statusMap: Record<string, 'running' | 'stopped' | 'unknown'> = {};
|
|
|
|
// Group containers by server (local vs remote)
|
|
const localContainers: string[] = [];
|
|
const remoteContainers: Array<{containerId: string, server: any}> = [];
|
|
|
|
for (const container of containers) {
|
|
if (!container.serverId || !container.server) {
|
|
localContainers.push(container.containerId);
|
|
} else {
|
|
remoteContainers.push({
|
|
containerId: container.containerId,
|
|
server: container.server
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check local containers
|
|
if (localContainers.length > 0) {
|
|
const localStatuses = await getLocalContainerStatuses(localContainers);
|
|
Object.assign(statusMap, localStatuses);
|
|
}
|
|
|
|
// Check remote containers - group by server and make one call per server
|
|
const serverGroups: Record<string, Array<{containerId: string, server: any}>> = {};
|
|
|
|
for (const { containerId, server } of remoteContainers) {
|
|
const serverKey = `${server.id}-${server.name}`;
|
|
serverGroups[serverKey] ??= [];
|
|
serverGroups[serverKey].push({ containerId, server });
|
|
}
|
|
|
|
// Make one call per server
|
|
for (const [serverKey, containers] of Object.entries(serverGroups)) {
|
|
try {
|
|
if (containers.length === 0) continue;
|
|
|
|
const server = containers[0]?.server;
|
|
if (!server) continue;
|
|
|
|
const containerIds = containers.map(c => c.containerId).filter(Boolean);
|
|
const serverStatuses = await getRemoteContainerStatuses(containerIds, server as Server);
|
|
|
|
// Merge the results
|
|
Object.assign(statusMap, serverStatuses);
|
|
} catch (error) {
|
|
console.error(`Error checking statuses for server ${serverKey}:`, error);
|
|
// Set all containers for this server to unknown
|
|
for (const container of containers) {
|
|
if (container.containerId) {
|
|
statusMap[container.containerId] = 'unknown';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
statusMap
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in getContainerStatuses:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to fetch container statuses',
|
|
statusMap: {}
|
|
};
|
|
}
|
|
})
|
|
});
|