|
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'; |
@@ -31,6 +31,11 @@ export function InstalledScriptsTab() { |
31 | 31 | const [editFormData, setEditFormData] = useState<{ script_name: string; container_id: string }>({ script_name: '', container_id: '' }); |
32 | 32 | const [showAddForm, setShowAddForm] = useState(false); |
33 | 33 | 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); |
34 | 39 |
|
35 | 40 | // Fetch installed scripts |
36 | 41 | const { data: scriptsData, refetch: refetchScripts, isLoading } = api.installedScripts.getAllInstalledScripts.useQuery(); |
@@ -68,10 +73,87 @@ export function InstalledScriptsTab() { |
68 | 73 | } |
69 | 74 | }); |
70 | 75 |
|
| 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 | + |
71 | 144 |
|
72 | 145 | const scripts: InstalledScript[] = (scriptsData?.scripts as InstalledScript[]) ?? []; |
73 | 146 | const stats = statsData?.stats; |
74 | 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 | + |
75 | 157 | // Filter scripts based on search and filters |
76 | 158 | const filteredScripts = scripts.filter((script: InstalledScript) => { |
77 | 159 | const matchesSearch = script.script_name.toLowerCase().includes(searchTerm.toLowerCase()) || |
@@ -197,6 +279,25 @@ export function InstalledScriptsTab() { |
197 | 279 | setAddFormData({ script_name: '', container_id: '', server_id: 'local' }); |
198 | 280 | }; |
199 | 281 |
|
| 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 | + |
200 | 301 |
|
201 | 302 | const formatDate = (dateString: string) => { |
202 | 303 | return new Date(dateString).toLocaleString(); |
@@ -251,15 +352,22 @@ export function InstalledScriptsTab() { |
251 | 352 | </div> |
252 | 353 | )} |
253 | 354 |
|
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"> |
256 | 357 | <Button |
257 | 358 | onClick={() => setShowAddForm(!showAddForm)} |
258 | 359 | variant={showAddForm ? "outline" : "default"} |
259 | 360 | size="default" |
260 | 361 | > |
261 | 362 | {showAddForm ? 'Cancel Add Script' : '+ Add Manual Script Entry'} |
262 | 363 | </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> |
263 | 371 | </div> |
264 | 372 |
|
265 | 373 | {/* Add Script Form */} |
@@ -331,6 +439,145 @@ export function InstalledScriptsTab() { |
331 | 439 | </div> |
332 | 440 | )} |
333 | 441 |
|
| 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 "community-script")</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 "community-script" 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 | + |
334 | 581 | {/* Filters */} |
335 | 582 | <div className="space-y-4"> |
336 | 583 | {/* Search Input - Full Width on Mobile */} |
|
0 commit comments