Skip to content

Commit 762d748

Browse files
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
1 parent aaa09b4 commit 762d748

File tree

3 files changed

+248
-3
lines changed

3 files changed

+248
-3
lines changed

src/app/_components/InstalledScriptsTab.tsx

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface InstalledScript {
2222
installation_date: string;
2323
status: 'in_progress' | 'success' | 'failed';
2424
output_log: string | null;
25+
container_status?: 'running' | 'stopped' | 'unknown';
2526
}
2627

2728
export function InstalledScriptsTab() {
@@ -40,6 +41,7 @@ export function InstalledScriptsTab() {
4041
const [autoDetectStatus, setAutoDetectStatus] = useState<{ type: 'success' | 'error' | null; message: string }>({ type: null, message: '' });
4142
const [cleanupStatus, setCleanupStatus] = useState<{ type: 'success' | 'error' | null; message: string }>({ type: null, message: '' });
4243
const cleanupRunRef = useRef(false);
44+
const [containerStatuses, setContainerStatuses] = useState<Record<string, 'running' | 'stopped' | 'unknown'>>({});
4345

4446
// Fetch installed scripts
4547
const { data: scriptsData, refetch: refetchScripts, isLoading } = api.installedScripts.getAllInstalledScripts.useQuery();
@@ -114,6 +116,18 @@ export function InstalledScriptsTab() {
114116
}
115117
});
116118

119+
// Get container statuses mutation
120+
const containerStatusMutation = api.installedScripts.getContainerStatuses.useMutation({
121+
onSuccess: (data) => {
122+
if (data.success) {
123+
setContainerStatuses(data.statusMap);
124+
}
125+
},
126+
onError: (error) => {
127+
console.error('Error fetching container statuses:', error);
128+
}
129+
});
130+
117131
// Cleanup orphaned scripts mutation
118132
const cleanupMutation = api.installedScripts.cleanupOrphanedScripts.useMutation({
119133
onSuccess: (data) => {
@@ -149,6 +163,28 @@ export function InstalledScriptsTab() {
149163
const scripts: InstalledScript[] = (scriptsData?.scripts as InstalledScript[]) ?? [];
150164
const stats = statsData?.stats;
151165

166+
// Function to fetch container statuses
167+
const fetchContainerStatuses = () => {
168+
const containersWithIds = scripts
169+
.filter(script => script.container_id)
170+
.map(script => ({
171+
containerId: script.container_id!,
172+
serverId: script.server_id ?? undefined,
173+
server: script.server_id ? {
174+
id: script.server_id,
175+
name: script.server_name!,
176+
ip: script.server_ip!,
177+
user: script.server_user!,
178+
password: script.server_password!,
179+
auth_type: 'password' // Default to password auth
180+
} : undefined
181+
}));
182+
183+
if (containersWithIds.length > 0) {
184+
containerStatusMutation.mutate({ containers: containersWithIds });
185+
}
186+
};
187+
152188
// Run cleanup when component mounts and scripts are loaded (only once)
153189
useEffect(() => {
154190
if (scripts.length > 0 && serversData?.servers && !cleanupMutation.isPending && !cleanupRunRef.current) {
@@ -158,8 +194,23 @@ export function InstalledScriptsTab() {
158194
}
159195
}, [scripts.length, serversData?.servers, cleanupMutation]);
160196

197+
// Auto-refresh container statuses every 60 seconds
198+
useEffect(() => {
199+
if (scripts.length > 0) {
200+
fetchContainerStatuses(); // Initial fetch
201+
const interval = setInterval(fetchContainerStatuses, 60000); // Every 60 seconds
202+
return () => clearInterval(interval);
203+
}
204+
}, [scripts.length]);
205+
206+
// Update scripts with container statuses
207+
const scriptsWithStatus = scripts.map(script => ({
208+
...script,
209+
container_status: script.container_id ? containerStatuses[script.container_id] || 'unknown' : undefined
210+
}));
211+
161212
// Filter and sort scripts
162-
const filteredScripts = scripts
213+
const filteredScripts = scriptsWithStatus
163214
.filter((script: InstalledScript) => {
164215
const matchesSearch = script.script_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
165216
(script.container_id?.includes(searchTerm) ?? false) ||
@@ -810,7 +861,27 @@ export function InstalledScriptsTab() {
810861
/>
811862
) : (
812863
script.container_id ? (
813-
<span className="text-sm font-mono text-foreground">{String(script.container_id)}</span>
864+
<div className="flex items-center space-x-2">
865+
<span className="text-sm font-mono text-foreground">{String(script.container_id)}</span>
866+
{script.container_status && (
867+
<div className="flex items-center space-x-1">
868+
<div className={`w-2 h-2 rounded-full ${
869+
script.container_status === 'running' ? 'bg-green-500' :
870+
script.container_status === 'stopped' ? 'bg-red-500' :
871+
'bg-gray-400'
872+
}`}></div>
873+
<span className={`text-xs font-medium ${
874+
script.container_status === 'running' ? 'text-green-700 dark:text-green-300' :
875+
script.container_status === 'stopped' ? 'text-red-700 dark:text-red-300' :
876+
'text-gray-500 dark:text-gray-400'
877+
}`}>
878+
{script.container_status === 'running' ? 'Running' :
879+
script.container_status === 'stopped' ? 'Stopped' :
880+
'Unknown'}
881+
</span>
882+
</div>
883+
)}
884+
</div>
814885
) : (
815886
<span className="text-sm text-muted-foreground">-</span>
816887
)

src/app/_components/ScriptInstallationCard.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface InstalledScript {
1818
installation_date: string;
1919
status: 'in_progress' | 'success' | 'failed';
2020
output_log: string | null;
21+
container_status?: 'running' | 'stopped' | 'unknown';
2122
}
2223

2324
interface ScriptInstallationCardProps {
@@ -99,7 +100,29 @@ export function ScriptInstallationCard({
99100
/>
100101
) : (
101102
<div className="text-sm font-mono text-foreground break-all">
102-
{script.container_id ?? '-'}
103+
{script.container_id ? (
104+
<div className="flex items-center space-x-2">
105+
<span>{script.container_id}</span>
106+
{script.container_status && (
107+
<div className="flex items-center space-x-1">
108+
<div className={`w-2 h-2 rounded-full ${
109+
script.container_status === 'running' ? 'bg-green-500' :
110+
script.container_status === 'stopped' ? 'bg-red-500' :
111+
'bg-gray-400'
112+
}`}></div>
113+
<span className={`text-xs font-medium ${
114+
script.container_status === 'running' ? 'text-green-700 dark:text-green-300' :
115+
script.container_status === 'stopped' ? 'text-red-700 dark:text-red-300' :
116+
'text-gray-500 dark:text-gray-400'
117+
}`}>
118+
{script.container_status === 'running' ? 'Running' :
119+
script.container_status === 'stopped' ? 'Stopped' :
120+
'Unknown'}
121+
</span>
122+
</div>
123+
)}
124+
</div>
125+
) : '-'}
103126
</div>
104127
)}
105128
</div>

src/server/api/routers/installedScripts.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,90 @@
11
import { z } from "zod";
22
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
33
import { getDatabase } from "~/server/database";
4+
import { exec } from "child_process";
5+
import { promisify } from "util";
6+
import { getSSHExecutionService } from "~/server/ssh-execution-service";
7+
8+
const execAsync = promisify(exec);
9+
10+
// Helper function to check local container statuses
11+
async function getLocalContainerStatuses(containerIds: string[]): Promise<Record<string, 'running' | 'stopped' | 'unknown'>> {
12+
try {
13+
const { stdout } = await execAsync('pct list');
14+
const statusMap: Record<string, 'running' | 'stopped' | 'unknown'> = {};
15+
16+
// Parse pct list output
17+
const lines = stdout.trim().split('\n');
18+
const dataLines = lines.slice(1); // Skip header
19+
20+
for (const line of dataLines) {
21+
const parts = line.trim().split(/\s+/);
22+
if (parts.length >= 2) {
23+
const vmid = parts[0];
24+
const status = parts[1];
25+
26+
if (containerIds.includes(vmid)) {
27+
statusMap[vmid] = status === 'running' ? 'running' : 'stopped';
28+
}
29+
}
30+
}
31+
32+
// Set unknown for containers not found in pct list
33+
for (const containerId of containerIds) {
34+
if (!(containerId in statusMap)) {
35+
statusMap[containerId] = 'unknown';
36+
}
37+
}
38+
39+
return statusMap;
40+
} catch (error) {
41+
console.error('Error checking local container statuses:', error);
42+
// Return unknown for all containers on error
43+
const statusMap: Record<string, 'running' | 'stopped' | 'unknown'> = {};
44+
for (const containerId of containerIds) {
45+
statusMap[containerId] = 'unknown';
46+
}
47+
return statusMap;
48+
}
49+
}
50+
51+
// Helper function to check remote container status
52+
async function getRemoteContainerStatus(containerId: string, server: any): Promise<'running' | 'stopped' | 'unknown'> {
53+
return new Promise((resolve) => {
54+
const sshService = getSSHExecutionService();
55+
56+
sshService.executeCommand(
57+
server,
58+
'pct list',
59+
(data: string) => {
60+
// Parse the output to find the specific container
61+
const lines = data.trim().split('\n');
62+
const dataLines = lines.slice(1); // Skip header
63+
64+
for (const line of dataLines) {
65+
const parts = line.trim().split(/\s+/);
66+
if (parts.length >= 2 && parts[0] === containerId) {
67+
const status = parts[1];
68+
resolve(status === 'running' ? 'running' : 'stopped');
69+
return;
70+
}
71+
}
72+
73+
// Container not found in the list
74+
resolve('unknown');
75+
},
76+
(error: string) => {
77+
console.error(`Error checking remote container ${containerId}:`, error);
78+
resolve('unknown');
79+
},
80+
(exitCode: number) => {
81+
if (exitCode !== 0) {
82+
resolve('unknown');
83+
}
84+
}
85+
);
86+
});
87+
}
488

589
export const installedScriptsRouter = createTRPCRouter({
690
// Get all installed scripts
@@ -540,5 +624,72 @@ export const installedScriptsRouter = createTRPCRouter({
540624
deletedScripts: []
541625
};
542626
}
627+
}),
628+
629+
// Get container running statuses
630+
getContainerStatuses: publicProcedure
631+
.input(z.object({
632+
containers: z.array(z.object({
633+
containerId: z.string(),
634+
serverId: z.number().optional(),
635+
server: z.object({
636+
id: z.number(),
637+
name: z.string(),
638+
ip: z.string(),
639+
user: z.string(),
640+
password: z.string(),
641+
auth_type: z.string()
642+
}).optional()
643+
}))
644+
}))
645+
.mutation(async ({ input }) => {
646+
try {
647+
const { containers } = input;
648+
const statusMap: Record<string, 'running' | 'stopped' | 'unknown'> = {};
649+
650+
// Group containers by server (local vs remote)
651+
const localContainers: string[] = [];
652+
const remoteContainers: Array<{containerId: string, server: any}> = [];
653+
654+
for (const container of containers) {
655+
if (!container.serverId || !container.server) {
656+
localContainers.push(container.containerId);
657+
} else {
658+
remoteContainers.push({
659+
containerId: container.containerId,
660+
server: container.server
661+
});
662+
}
663+
}
664+
665+
// Check local containers
666+
if (localContainers.length > 0) {
667+
const localStatuses = await getLocalContainerStatuses(localContainers);
668+
Object.assign(statusMap, localStatuses);
669+
}
670+
671+
// Check remote containers
672+
for (const { containerId, server } of remoteContainers) {
673+
try {
674+
const remoteStatus = await getRemoteContainerStatus(containerId, server);
675+
statusMap[containerId] = remoteStatus;
676+
} catch (error) {
677+
console.error(`Error checking status for container ${containerId} on server ${server.name}:`, error);
678+
statusMap[containerId] = 'unknown';
679+
}
680+
}
681+
682+
return {
683+
success: true,
684+
statusMap
685+
};
686+
} catch (error) {
687+
console.error('Error in getContainerStatuses:', error);
688+
return {
689+
success: false,
690+
error: error instanceof Error ? error.message : 'Failed to fetch container statuses',
691+
statusMap: {}
692+
};
693+
}
543694
})
544695
});

0 commit comments

Comments
 (0)