Skip to content

Commit 53b5074

Browse files
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
1 parent aaa09b4 commit 53b5074

File tree

3 files changed

+340
-10
lines changed

3 files changed

+340
-10
lines changed

src/app/_components/InstalledScriptsTab.tsx

Lines changed: 133 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useState, useEffect, useRef } from 'react';
3+
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
44
import { api } from '~/trpc/react';
55
import { Terminal } from './Terminal';
66
import { StatusBadge } from './Badge';
@@ -22,13 +22,14 @@ 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() {
2829
const [searchTerm, setSearchTerm] = useState('');
2930
const [statusFilter, setStatusFilter] = useState<'all' | 'success' | 'failed' | 'in_progress'>('all');
3031
const [serverFilter, setServerFilter] = useState<string>('all');
31-
const [sortField, setSortField] = useState<'script_name' | 'container_id' | 'server_name' | 'status' | 'installation_date'>('script_name');
32+
const [sortField, setSortField] = useState<'script_name' | 'container_id' | 'server_name' | 'status' | 'installation_date'>('server_name');
3233
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
3334
const [updatingScript, setUpdatingScript] = useState<{ id: number; containerId: string; server?: any } | null>(null);
3435
const [editingScriptId, setEditingScriptId] = useState<number | null>(null);
@@ -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) => {
@@ -146,9 +160,33 @@ export function InstalledScriptsTab() {
146160
});
147161

148162

149-
const scripts: InstalledScript[] = (scriptsData?.scripts as InstalledScript[]) ?? [];
163+
const scripts: InstalledScript[] = useMemo(() => (scriptsData?.scripts as InstalledScript[]) ?? [], [scriptsData?.scripts]);
150164
const stats = statsData?.stats;
151165

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

199+
// Auto-refresh container statuses every 60 seconds
200+
useEffect(() => {
201+
if (scripts.length > 0) {
202+
fetchContainerStatuses(); // Initial fetch
203+
const interval = setInterval(fetchContainerStatuses, 60000); // Every 60 seconds
204+
return () => clearInterval(interval);
205+
}
206+
}, [scripts.length, fetchContainerStatuses]);
207+
208+
// Trigger status check when component becomes visible (tab is active)
209+
useEffect(() => {
210+
if (scripts.length > 0) {
211+
// Small delay to ensure component is fully rendered
212+
const timeoutId = setTimeout(() => {
213+
fetchContainerStatuses();
214+
}, 100);
215+
216+
return () => clearTimeout(timeoutId);
217+
}
218+
}, [scripts.length, fetchContainerStatuses]); // Include dependencies
219+
220+
// Also trigger status check when scripts data loads
221+
useEffect(() => {
222+
if (scripts.length > 0 && !isLoading) {
223+
console.log('Scripts data loaded, triggering status check');
224+
fetchContainerStatuses();
225+
}
226+
}, [scriptsData, isLoading, scripts.length, fetchContainerStatuses]);
227+
228+
// Update scripts with container statuses
229+
const scriptsWithStatus = scripts.map(script => ({
230+
...script,
231+
container_status: script.container_id ? containerStatuses[script.container_id] ?? 'unknown' : undefined
232+
}));
233+
161234
// Filter and sort scripts
162-
const filteredScripts = scripts
235+
const filteredScripts = scriptsWithStatus
163236
.filter((script: InstalledScript) => {
164237
const matchesSearch = script.script_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
165238
(script.container_id?.includes(searchTerm) ?? false) ||
@@ -174,6 +247,33 @@ export function InstalledScriptsTab() {
174247
return matchesSearch && matchesStatus && matchesServer;
175248
})
176249
.sort((a: InstalledScript, b: InstalledScript) => {
250+
// Default sorting: group by server, then by container ID
251+
if (sortField === 'server_name') {
252+
const aServer = a.server_name ?? 'Local';
253+
const bServer = b.server_name ?? 'Local';
254+
255+
// First sort by server name
256+
if (aServer !== bServer) {
257+
return sortDirection === 'asc' ?
258+
aServer.localeCompare(bServer) :
259+
bServer.localeCompare(aServer);
260+
}
261+
262+
// If same server, sort by container ID
263+
const aContainerId = a.container_id ?? '';
264+
const bContainerId = b.container_id ?? '';
265+
266+
if (aContainerId !== bContainerId) {
267+
// Convert to numbers for proper numeric sorting
268+
const aNum = parseInt(aContainerId) || 0;
269+
const bNum = parseInt(bContainerId) || 0;
270+
return sortDirection === 'asc' ? aNum - bNum : bNum - aNum;
271+
}
272+
273+
return 0;
274+
}
275+
276+
// For other sort fields, use the original logic
177277
let aValue: any;
178278
let bValue: any;
179279

@@ -186,10 +286,6 @@ export function InstalledScriptsTab() {
186286
aValue = a.container_id ?? '';
187287
bValue = b.container_id ?? '';
188288
break;
189-
case 'server_name':
190-
aValue = a.server_name ?? 'Local';
191-
bValue = b.server_name ?? 'Local';
192-
break;
193289
case 'status':
194290
aValue = a.status;
195291
bValue = b.status;
@@ -419,6 +515,14 @@ export function InstalledScriptsTab() {
419515
>
420516
{showAutoDetectForm ? 'Cancel Auto-Detect' : '🔍 Auto-Detect LXC Containers (Must contain a tag with "community-script")'}
421517
</Button>
518+
<Button
519+
onClick={fetchContainerStatuses}
520+
disabled={containerStatusMutation.isPending || scripts.length === 0}
521+
variant="outline"
522+
size="default"
523+
>
524+
{containerStatusMutation.isPending ? '🔄 Checking...' : '🔄 Refresh Container Status'}
525+
</Button>
422526
</div>
423527

424528
{/* Add Script Form */}
@@ -810,7 +914,27 @@ export function InstalledScriptsTab() {
810914
/>
811915
) : (
812916
script.container_id ? (
813-
<span className="text-sm font-mono text-foreground">{String(script.container_id)}</span>
917+
<div className="flex items-center space-x-2">
918+
<span className="text-sm font-mono text-foreground">{String(script.container_id)}</span>
919+
{script.container_status && (
920+
<div className="flex items-center space-x-1">
921+
<div className={`w-2 h-2 rounded-full ${
922+
script.container_status === 'running' ? 'bg-green-500' :
923+
script.container_status === 'stopped' ? 'bg-red-500' :
924+
'bg-gray-400'
925+
}`}></div>
926+
<span className={`text-xs font-medium ${
927+
script.container_status === 'running' ? 'text-green-700 dark:text-green-300' :
928+
script.container_status === 'stopped' ? 'text-red-700 dark:text-red-300' :
929+
'text-gray-500 dark:text-gray-400'
930+
}`}>
931+
{script.container_status === 'running' ? 'Running' :
932+
script.container_status === 'stopped' ? 'Stopped' :
933+
'Unknown'}
934+
</span>
935+
</div>
936+
)}
937+
</div>
814938
) : (
815939
<span className="text-sm text-muted-foreground">-</span>
816940
)

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>

0 commit comments

Comments
 (0)