|
1 | 1 | 'use client'; |
2 | 2 |
|
3 | | -import { useState } from 'react'; |
| 3 | +import { useState, useEffect, useRef } from 'react'; |
4 | 4 | import { api } from '~/trpc/react'; |
5 | 5 | import { Terminal } from './Terminal'; |
6 | 6 | import { StatusBadge } from './Badge'; |
@@ -34,6 +34,8 @@ export function InstalledScriptsTab() { |
34 | 34 | const [showAutoDetectForm, setShowAutoDetectForm] = useState(false); |
35 | 35 | const [autoDetectServerId, setAutoDetectServerId] = useState<string>(''); |
36 | 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); |
37 | 39 |
|
38 | 40 | // Fetch installed scripts |
39 | 41 | const { data: scriptsData, refetch: refetchScripts, isLoading } = api.installedScripts.getAllInstalledScripts.useQuery(); |
@@ -78,34 +80,80 @@ export function InstalledScriptsTab() { |
78 | 80 | void refetchScripts(); |
79 | 81 | setShowAutoDetectForm(false); |
80 | 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 | + |
81 | 91 | setAutoDetectStatus({ |
82 | 92 | type: 'success', |
83 | | - message: data.message || 'Auto-detection completed successfully!' |
| 93 | + message: statusMessage |
84 | 94 | }); |
85 | | - // Clear status after 5 seconds |
86 | | - setTimeout(() => setAutoDetectStatus({ type: null, message: '' }), 5000); |
| 95 | + // Clear status after 8 seconds (longer for detailed info) |
| 96 | + setTimeout(() => setAutoDetectStatus({ type: null, message: '' }), 8000); |
87 | 97 | }, |
88 | 98 | onError: (error) => { |
89 | 99 | console.error('Auto-detect mutation error:', error); |
90 | 100 | console.error('Error details:', { |
91 | 101 | message: error.message, |
92 | | - cause: error.cause, |
93 | | - stack: error.stack, |
94 | 102 | data: error.data |
95 | 103 | }); |
96 | 104 | setAutoDetectStatus({ |
97 | 105 | type: 'error', |
98 | | - message: error.message || 'Auto-detection failed. Please try again.' |
| 106 | + message: error.message ?? 'Auto-detection failed. Please try again.' |
99 | 107 | }); |
100 | 108 | // Clear status after 5 seconds |
101 | 109 | setTimeout(() => setAutoDetectStatus({ type: null, message: '' }), 5000); |
102 | 110 | } |
103 | 111 | }); |
104 | 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 | + |
105 | 144 |
|
106 | 145 | const scripts: InstalledScript[] = (scriptsData?.scripts as InstalledScript[]) ?? []; |
107 | 146 | const stats = statsData?.stats; |
108 | 147 |
|
| 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 | + |
109 | 157 | // Filter scripts based on search and filters |
110 | 158 | const filteredScripts = scripts.filter((script: InstalledScript) => { |
111 | 159 | const matchesSearch = script.script_name.toLowerCase().includes(searchTerm.toLowerCase()) || |
@@ -391,60 +439,97 @@ export function InstalledScriptsTab() { |
391 | 439 | </div> |
392 | 440 | )} |
393 | 441 |
|
394 | | - {/* Auto-Detect Status Message */} |
395 | | - {autoDetectStatus.type && ( |
396 | | - <div className={`mb-4 p-4 rounded-lg border ${ |
397 | | - autoDetectStatus.type === 'success' |
398 | | - ? 'bg-green-50 dark:bg-green-950/20 border-green-200 dark:border-green-800' |
399 | | - : 'bg-red-50 dark:bg-red-950/20 border-red-200 dark:border-red-800' |
400 | | - }`}> |
401 | | - <div className="flex items-center"> |
402 | | - <div className="flex-shrink-0"> |
403 | | - {autoDetectStatus.type === 'success' ? ( |
404 | | - <svg className="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor"> |
405 | | - <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" /> |
406 | | - </svg> |
407 | | - ) : ( |
408 | | - <svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor"> |
409 | | - <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" /> |
410 | | - </svg> |
411 | | - )} |
| 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> |
412 | 474 | </div> |
413 | | - <div className="ml-3"> |
414 | | - <p className={`text-sm font-medium ${ |
415 | | - autoDetectStatus.type === 'success' |
416 | | - ? 'text-green-800 dark:text-green-200' |
417 | | - : 'text-red-800 dark:text-red-200' |
418 | | - }`}> |
419 | | - {autoDetectStatus.message} |
420 | | - </p> |
| 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> |
421 | 506 | </div> |
422 | | - </div> |
| 507 | + )} |
423 | 508 | </div> |
424 | 509 | )} |
425 | 510 |
|
426 | 511 | {/* Auto-Detect LXC Containers Form */} |
427 | 512 | {showAutoDetectForm && ( |
428 | 513 | <div className="mb-6 p-4 sm:p-6 bg-card rounded-lg border border-border shadow-sm"> |
429 | | - <h3 className="text-lg font-semibold text-foreground mb-4 sm:mb-6">Auto-Detect LXC Containers (Must contain a tag with "community-script")</h3> |
| 514 | + <h3 className="text-lg font-semibold text-foreground mb-4 sm:mb-6">Auto-Detect LXC Containers (Must contain a tag with "community-script")</h3> |
430 | 515 | <div className="space-y-4 sm:space-y-6"> |
431 | | - <div className="bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4"> |
| 516 | + <div className="bg-slate-50 dark:bg-slate-900/30 border border-slate-200 dark:border-slate-700 rounded-lg p-4"> |
432 | 517 | <div className="flex items-start"> |
433 | 518 | <div className="flex-shrink-0"> |
434 | | - <svg className="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor"> |
| 519 | + <svg className="h-5 w-5 text-slate-500 dark:text-slate-400" viewBox="0 0 20 20" fill="currentColor"> |
435 | 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" /> |
436 | 521 | </svg> |
437 | 522 | </div> |
438 | 523 | <div className="ml-3"> |
439 | | - <h4 className="text-sm font-medium text-blue-800 dark:text-blue-200"> |
| 524 | + <h4 className="text-sm font-medium text-slate-700 dark:text-slate-300"> |
440 | 525 | How it works |
441 | 526 | </h4> |
442 | | - <div className="mt-2 text-sm text-blue-700 dark:text-blue-300"> |
| 527 | + <div className="mt-2 text-sm text-slate-600 dark:text-slate-400"> |
443 | 528 | <p>This feature will:</p> |
444 | 529 | <ul className="list-disc list-inside mt-1 space-y-1"> |
445 | 530 | <li>Connect to the selected server via SSH</li> |
446 | 531 | <li>Scan all LXC config files in /etc/pve/lxc/</li> |
447 | | - <li>Find containers with "community-script" in their tags</li> |
| 532 | + <li>Find containers with "community-script" in their tags</li> |
448 | 533 | <li>Extract the container ID and hostname</li> |
449 | 534 | <li>Add them as installed script entries</li> |
450 | 535 | </ul> |
|
0 commit comments