feat: Add container running status indicators with auto-refresh (#123)
* 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
This commit is contained in:
committed by
GitHub
parent
aaa09b4745
commit
53b5074f35
@@ -1,6 +1,100 @@
|
||||
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
|
||||
@@ -540,5 +634,94 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
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: {}
|
||||
};
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user