Skip to content

Commit 8b630c9

Browse files
feat: Add LXC auto-detection and cleanup of orphaned LXC (#80)
* feat: Add auto-detect LXC containers feature with improved UX - Add auto-detection for LXC containers with 'community-script' tag - SSH to Proxmox servers and scan /etc/pve/lxc/ config files - Extract container ID and hostname from config files - Automatically create installed script records for detected containers - Replace alert popups with modern status messages - Add visual feedback with success/error states - Auto-close form on successful detection - Add clear UI indicators for community-script tag requirement - Improve error handling and logging for better debugging - Support both local and SSH execution modes * feat: Add automatic cleanup and duplicate prevention for LXC auto-detection - Add automatic cleanup of orphaned LXC container scripts on tab load - Implement duplicate checking to prevent re-adding existing scripts - Replace flashy blue messages with subtle slate color scheme - Add comprehensive status messages for cleanup and auto-detection - Fix all ESLint errors and warnings - Improve user experience with non-intrusive feedback - Add detailed logging for debugging cleanup process - Support both success and error states with appropriate styling
1 parent 5eaafbd commit 8b630c9

File tree

3 files changed

+594
-32
lines changed

3 files changed

+594
-32
lines changed

server.log

Lines changed: 0 additions & 29 deletions
This file was deleted.

src/app/_components/InstalledScriptsTab.tsx

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

3-
import { useState } from 'react';
3+
import { useState, useEffect, useRef } from 'react';
44
import { api } from '~/trpc/react';
55
import { Terminal } from './Terminal';
66
import { StatusBadge } from './Badge';
@@ -31,6 +31,11 @@ export function InstalledScriptsTab() {
3131
const [editFormData, setEditFormData] = useState<{ script_name: string; container_id: string }>({ script_name: '', container_id: '' });
3232
const [showAddForm, setShowAddForm] = useState(false);
3333
const [addFormData, setAddFormData] = useState<{ script_name: string; container_id: string; server_id: string }>({ script_name: '', container_id: '', server_id: 'local' });
34+
const [showAutoDetectForm, setShowAutoDetectForm] = useState(false);
35+
const [autoDetectServerId, setAutoDetectServerId] = useState<string>('');
36+
const [autoDetectStatus, setAutoDetectStatus] = useState<{ type: 'success' | 'error' | null; message: string }>({ type: null, message: '' });
37+
const [cleanupStatus, setCleanupStatus] = useState<{ type: 'success' | 'error' | null; message: string }>({ type: null, message: '' });
38+
const cleanupRunRef = useRef(false);
3439

3540
// Fetch installed scripts
3641
const { data: scriptsData, refetch: refetchScripts, isLoading } = api.installedScripts.getAllInstalledScripts.useQuery();
@@ -68,10 +73,87 @@ export function InstalledScriptsTab() {
6873
}
6974
});
7075

76+
// Auto-detect LXC containers mutation
77+
const autoDetectMutation = api.installedScripts.autoDetectLXCContainers.useMutation({
78+
onSuccess: (data) => {
79+
console.log('Auto-detect success:', data);
80+
void refetchScripts();
81+
setShowAutoDetectForm(false);
82+
setAutoDetectServerId('');
83+
84+
// Show detailed message about what was added/skipped
85+
let statusMessage = data.message ?? 'Auto-detection completed successfully!';
86+
if (data.skippedContainers && data.skippedContainers.length > 0) {
87+
const skippedNames = data.skippedContainers.map((c: any) => String(c.hostname)).join(', ');
88+
statusMessage += ` Skipped duplicates: ${skippedNames}`;
89+
}
90+
91+
setAutoDetectStatus({
92+
type: 'success',
93+
message: statusMessage
94+
});
95+
// Clear status after 8 seconds (longer for detailed info)
96+
setTimeout(() => setAutoDetectStatus({ type: null, message: '' }), 8000);
97+
},
98+
onError: (error) => {
99+
console.error('Auto-detect mutation error:', error);
100+
console.error('Error details:', {
101+
message: error.message,
102+
data: error.data
103+
});
104+
setAutoDetectStatus({
105+
type: 'error',
106+
message: error.message ?? 'Auto-detection failed. Please try again.'
107+
});
108+
// Clear status after 5 seconds
109+
setTimeout(() => setAutoDetectStatus({ type: null, message: '' }), 5000);
110+
}
111+
});
112+
113+
// Cleanup orphaned scripts mutation
114+
const cleanupMutation = api.installedScripts.cleanupOrphanedScripts.useMutation({
115+
onSuccess: (data) => {
116+
console.log('Cleanup success:', data);
117+
void refetchScripts();
118+
119+
if (data.deletedCount > 0) {
120+
setCleanupStatus({
121+
type: 'success',
122+
message: `Cleanup completed! Removed ${data.deletedCount} orphaned script(s): ${data.deletedScripts.join(', ')}`
123+
});
124+
} else {
125+
setCleanupStatus({
126+
type: 'success',
127+
message: 'Cleanup completed! No orphaned scripts found.'
128+
});
129+
}
130+
// Clear status after 8 seconds (longer for cleanup info)
131+
setTimeout(() => setCleanupStatus({ type: null, message: '' }), 8000);
132+
},
133+
onError: (error) => {
134+
console.error('Cleanup mutation error:', error);
135+
setCleanupStatus({
136+
type: 'error',
137+
message: error.message ?? 'Cleanup failed. Please try again.'
138+
});
139+
// Clear status after 5 seconds
140+
setTimeout(() => setCleanupStatus({ type: null, message: '' }), 5000);
141+
}
142+
});
143+
71144

72145
const scripts: InstalledScript[] = (scriptsData?.scripts as InstalledScript[]) ?? [];
73146
const stats = statsData?.stats;
74147

148+
// Run cleanup when component mounts and scripts are loaded (only once)
149+
useEffect(() => {
150+
if (scripts.length > 0 && serversData?.servers && !cleanupMutation.isPending && !cleanupRunRef.current) {
151+
console.log('Running automatic cleanup check...');
152+
cleanupRunRef.current = true;
153+
void cleanupMutation.mutate();
154+
}
155+
}, [scripts.length, serversData?.servers, cleanupMutation]);
156+
75157
// Filter scripts based on search and filters
76158
const filteredScripts = scripts.filter((script: InstalledScript) => {
77159
const matchesSearch = script.script_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
@@ -197,6 +279,25 @@ export function InstalledScriptsTab() {
197279
setAddFormData({ script_name: '', container_id: '', server_id: 'local' });
198280
};
199281

282+
const handleAutoDetect = () => {
283+
if (!autoDetectServerId) {
284+
return;
285+
}
286+
287+
if (autoDetectMutation.isPending) {
288+
return;
289+
}
290+
291+
setAutoDetectStatus({ type: null, message: '' });
292+
console.log('Starting auto-detect for server ID:', autoDetectServerId);
293+
autoDetectMutation.mutate({ serverId: Number(autoDetectServerId) });
294+
};
295+
296+
const handleCancelAutoDetect = () => {
297+
setShowAutoDetectForm(false);
298+
setAutoDetectServerId('');
299+
};
300+
200301

201302
const formatDate = (dateString: string) => {
202303
return new Date(dateString).toLocaleString();
@@ -251,15 +352,22 @@ export function InstalledScriptsTab() {
251352
</div>
252353
)}
253354

254-
{/* Add Script Button */}
255-
<div className="mb-4">
355+
{/* Add Script and Auto-Detect Buttons */}
356+
<div className="mb-4 flex flex-col sm:flex-row gap-3">
256357
<Button
257358
onClick={() => setShowAddForm(!showAddForm)}
258359
variant={showAddForm ? "outline" : "default"}
259360
size="default"
260361
>
261362
{showAddForm ? 'Cancel Add Script' : '+ Add Manual Script Entry'}
262363
</Button>
364+
<Button
365+
onClick={() => setShowAutoDetectForm(!showAutoDetectForm)}
366+
variant={showAutoDetectForm ? "outline" : "secondary"}
367+
size="default"
368+
>
369+
{showAutoDetectForm ? 'Cancel Auto-Detect' : '🔍 Auto-Detect LXC Containers (Must contain a tag with "community-script")'}
370+
</Button>
263371
</div>
264372

265373
{/* Add Script Form */}
@@ -331,6 +439,145 @@ export function InstalledScriptsTab() {
331439
</div>
332440
)}
333441

442+
{/* Status Messages */}
443+
{(autoDetectStatus.type ?? cleanupStatus.type) && (
444+
<div className="mb-4 space-y-2">
445+
{/* Auto-Detect Status Message */}
446+
{autoDetectStatus.type && (
447+
<div className={`p-4 rounded-lg border ${
448+
autoDetectStatus.type === 'success'
449+
? 'bg-green-50 dark:bg-green-950/20 border-green-200 dark:border-green-800'
450+
: 'bg-red-50 dark:bg-red-950/20 border-red-200 dark:border-red-800'
451+
}`}>
452+
<div className="flex items-center">
453+
<div className="flex-shrink-0">
454+
{autoDetectStatus.type === 'success' ? (
455+
<svg className="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
456+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
457+
</svg>
458+
) : (
459+
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
460+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
461+
</svg>
462+
)}
463+
</div>
464+
<div className="ml-3">
465+
<p className={`text-sm font-medium ${
466+
autoDetectStatus.type === 'success'
467+
? 'text-green-800 dark:text-green-200'
468+
: 'text-red-800 dark:text-red-200'
469+
}`}>
470+
{autoDetectStatus.message}
471+
</p>
472+
</div>
473+
</div>
474+
</div>
475+
)}
476+
477+
{/* Cleanup Status Message */}
478+
{cleanupStatus.type && (
479+
<div className={`p-4 rounded-lg border ${
480+
cleanupStatus.type === 'success'
481+
? 'bg-slate-50 dark:bg-slate-900/50 border-slate-200 dark:border-slate-700'
482+
: 'bg-red-50 dark:bg-red-950/20 border-red-200 dark:border-red-800'
483+
}`}>
484+
<div className="flex items-center">
485+
<div className="flex-shrink-0">
486+
{cleanupStatus.type === 'success' ? (
487+
<svg className="h-5 w-5 text-slate-500 dark:text-slate-400" viewBox="0 0 20 20" fill="currentColor">
488+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
489+
</svg>
490+
) : (
491+
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
492+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
493+
</svg>
494+
)}
495+
</div>
496+
<div className="ml-3">
497+
<p className={`text-sm font-medium ${
498+
cleanupStatus.type === 'success'
499+
? 'text-slate-700 dark:text-slate-300'
500+
: 'text-red-800 dark:text-red-200'
501+
}`}>
502+
{cleanupStatus.message}
503+
</p>
504+
</div>
505+
</div>
506+
</div>
507+
)}
508+
</div>
509+
)}
510+
511+
{/* Auto-Detect LXC Containers Form */}
512+
{showAutoDetectForm && (
513+
<div className="mb-6 p-4 sm:p-6 bg-card rounded-lg border border-border shadow-sm">
514+
<h3 className="text-lg font-semibold text-foreground mb-4 sm:mb-6">Auto-Detect LXC Containers (Must contain a tag with &quot;community-script&quot;)</h3>
515+
<div className="space-y-4 sm:space-y-6">
516+
<div className="bg-slate-50 dark:bg-slate-900/30 border border-slate-200 dark:border-slate-700 rounded-lg p-4">
517+
<div className="flex items-start">
518+
<div className="flex-shrink-0">
519+
<svg className="h-5 w-5 text-slate-500 dark:text-slate-400" viewBox="0 0 20 20" fill="currentColor">
520+
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
521+
</svg>
522+
</div>
523+
<div className="ml-3">
524+
<h4 className="text-sm font-medium text-slate-700 dark:text-slate-300">
525+
How it works
526+
</h4>
527+
<div className="mt-2 text-sm text-slate-600 dark:text-slate-400">
528+
<p>This feature will:</p>
529+
<ul className="list-disc list-inside mt-1 space-y-1">
530+
<li>Connect to the selected server via SSH</li>
531+
<li>Scan all LXC config files in /etc/pve/lxc/</li>
532+
<li>Find containers with &quot;community-script&quot; in their tags</li>
533+
<li>Extract the container ID and hostname</li>
534+
<li>Add them as installed script entries</li>
535+
</ul>
536+
</div>
537+
</div>
538+
</div>
539+
</div>
540+
541+
<div className="space-y-2">
542+
<label className="block text-sm font-medium text-foreground">
543+
Select Server *
544+
</label>
545+
<select
546+
value={autoDetectServerId}
547+
onChange={(e) => setAutoDetectServerId(e.target.value)}
548+
className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
549+
>
550+
<option value="">Choose a server...</option>
551+
{serversData?.servers?.map((server: any) => (
552+
<option key={server.id} value={server.id}>
553+
{server.name} ({server.ip})
554+
</option>
555+
))}
556+
</select>
557+
</div>
558+
</div>
559+
<div className="flex flex-col sm:flex-row justify-end gap-3 mt-4 sm:mt-6">
560+
<Button
561+
onClick={handleCancelAutoDetect}
562+
variant="outline"
563+
size="default"
564+
className="w-full sm:w-auto"
565+
>
566+
Cancel
567+
</Button>
568+
<Button
569+
onClick={handleAutoDetect}
570+
disabled={autoDetectMutation.isPending || !autoDetectServerId}
571+
variant="default"
572+
size="default"
573+
className="w-full sm:w-auto"
574+
>
575+
{autoDetectMutation.isPending ? '🔍 Scanning...' : '🔍 Start Auto-Detection'}
576+
</Button>
577+
</div>
578+
</div>
579+
)}
580+
334581
{/* Filters */}
335582
<div className="space-y-4">
336583
{/* Search Input - Full Width on Mobile */}

0 commit comments

Comments
 (0)