diff --git a/Backend/src/NexusRMM.Api/Controllers/AgentsController.cs b/Backend/src/NexusRMM.Api/Controllers/AgentsController.cs index b776da6..817ea31 100644 --- a/Backend/src/NexusRMM.Api/Controllers/AgentsController.cs +++ b/Backend/src/NexusRMM.Api/Controllers/AgentsController.cs @@ -16,7 +16,7 @@ public class AgentsController : ControllerBase { var agents = await _db.Agents .OrderBy(a => a.Hostname) - .Select(a => new { a.Id, a.Hostname, a.OsType, a.OsVersion, a.IpAddress, a.Status, a.AgentVersion, a.LastSeen, a.Tags }) + .Select(a => new { a.Id, a.Hostname, a.OsType, a.OsVersion, a.IpAddress, a.MacAddress, a.Status, a.AgentVersion, a.LastSeen, a.EnrolledAt, a.Tags }) .ToListAsync(); return Ok(agents); } @@ -29,14 +29,13 @@ public class AgentsController : ControllerBase } [HttpGet("{id:guid}/metrics")] - public async Task GetMetrics(Guid id, [FromQuery] int hours = 24) + public async Task GetMetrics(Guid id, [FromQuery] int limit = 100) { - var since = DateTime.UtcNow.AddHours(-hours); var metrics = await _db.AgentMetrics - .Where(m => m.AgentId == id && m.Timestamp >= since) + .Where(m => m.AgentId == id) .OrderByDescending(m => m.Timestamp) - .Take(1000) - .Select(m => new { m.Timestamp, m.Metrics }) + .Take(limit) + .Select(m => new { m.Id, m.AgentId, m.Timestamp, m.Metrics }) .ToListAsync(); return Ok(metrics); } diff --git a/Backend/src/NexusRMM.Api/Controllers/TasksController.cs b/Backend/src/NexusRMM.Api/Controllers/TasksController.cs index c5280e7..018e677 100644 --- a/Backend/src/NexusRMM.Api/Controllers/TasksController.cs +++ b/Backend/src/NexusRMM.Api/Controllers/TasksController.cs @@ -29,23 +29,21 @@ public class TasksController : ControllerBase return CreatedAtAction(nameof(GetById), new { id = task.Id }, task); } + [HttpGet] + public async Task GetAll([FromQuery] Guid? agentId = null) + { + var query = _db.Tasks.AsQueryable(); + if (agentId.HasValue) query = query.Where(t => t.AgentId == agentId.Value); + var tasks = await query.OrderByDescending(t => t.CreatedAt).Take(50).ToListAsync(); + return Ok(tasks); + } + [HttpGet("{id:guid}")] public async Task GetById(Guid id) { var task = await _db.Tasks.FindAsync(id); return task is null ? NotFound() : Ok(task); } - - [HttpGet("agent/{agentId:guid}")] - public async Task GetByAgent(Guid agentId) - { - var tasks = await _db.Tasks - .Where(t => t.AgentId == agentId) - .OrderByDescending(t => t.CreatedAt) - .Take(50) - .ToListAsync(); - return Ok(tasks); - } } public record CreateTaskRequest(Guid AgentId, TaskType Type, object? Payload); diff --git a/Backend/src/NexusRMM.Api/Program.cs b/Backend/src/NexusRMM.Api/Program.cs index fccd90e..63c6893 100644 --- a/Backend/src/NexusRMM.Api/Program.cs +++ b/Backend/src/NexusRMM.Api/Program.cs @@ -20,7 +20,12 @@ builder.Services.AddDbContext(options => builder.Services.AddGrpc(); builder.Services.AddSignalR(); -builder.Services.AddControllers(); +builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); + }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); diff --git a/Frontend/src/index.css b/Frontend/src/index.css index df0606e..fcce6c0 100644 --- a/Frontend/src/index.css +++ b/Frontend/src/index.css @@ -33,4 +33,9 @@ background-color: hsl(var(--background)); color: hsl(var(--foreground)); } + /* Dropdown-Optionen im Dark Mode sichtbar machen */ + select option { + background-color: hsl(var(--card)); + color: hsl(var(--foreground)); + } } diff --git a/Frontend/src/pages/SoftwarePage.tsx b/Frontend/src/pages/SoftwarePage.tsx index 58c3f40..5b00b54 100644 --- a/Frontend/src/pages/SoftwarePage.tsx +++ b/Frontend/src/pages/SoftwarePage.tsx @@ -15,13 +15,11 @@ export default function SoftwarePage() { const queryClient = useQueryClient() - // Fetch packages const { data: packages = [], isLoading } = useQuery({ queryKey: ['software-packages', filterOs], queryFn: () => softwarePackagesApi.list(filterOs === 'All' ? undefined : filterOs), }) - // Mutations const createMutation = useMutation({ mutationFn: (data: CreateSoftwarePackageRequest) => softwarePackagesApi.create(data), onSuccess: () => { @@ -96,134 +94,134 @@ export default function SoftwarePage() { } return ( -
-
- {/* Header */} -
-
- -

Software-Verwaltung

-
-

Verwalte Software-Pakete und deploye sie auf Agenten

+
+ {/* Header */} +
+
+ +

Software-Verwaltung

+

Verwalte Software-Pakete und deploye sie auf Agenten

+
- {/* Tabs */} -
- - + {/* Tabs */} +
+ + +
+ + {/* Messages */} + {successMessage && ( +
+ {successMessage}
+ )} + {errorMessage && ( +
+ {errorMessage} +
+ )} - {/* Messages */} - {successMessage && ( -
{successMessage}
- )} - {errorMessage && ( -
{errorMessage}
- )} - - {/* Tab Content */} - {activeTab === 'catalog' && ( -
- {/* Filter and Create Button */} -
-
- {(['All', 'Windows', 'Linux'] as const).map((os) => ( - - ))} -
- + {/* Catalog Tab */} + {activeTab === 'catalog' && ( +
+
+
+ {(['All', 'Windows', 'Linux'] as const).map((os) => ( + + ))}
+ +
- {/* Table */} - {isLoading ? ( -
Laden...
- ) : packages.length === 0 ? ( -
- Keine Pakete {filterOs !== 'All' && `für ${filterOs}`} vorhanden -
- ) : ( -
- - + {isLoading ? ( +
Laden...
+ ) : packages.length === 0 ? ( +
+ Keine Pakete {filterOs !== 'All' && `für ${filterOs}`} vorhanden +
+ ) : ( +
+
+
+ - - - - - - + + + + + + - + {packages.map((pkg) => ( - - - + + + - - - + + @@ -231,12 +229,12 @@ export default function SoftwarePage() {
NameVersionOS - Paketmanager - PaketnameAktionenNameVersionOSPaketmanagerPaketnameAktionen
{pkg.name}{pkg.version}
{pkg.name}{pkg.version} {pkg.osType} {pkg.packageManager}{pkg.packageName} + {pkg.packageManager}{pkg.packageName}
- )} -
- )} +
+ )} +
+ )} - {activeTab === 'deploy' && } -
+ {activeTab === 'deploy' && } {/* Create/Edit Modal */} {isCreateModalOpen && ( @@ -316,76 +314,72 @@ function CreatePackageModal({ availableManagers.includes(formData.packageManager as PackageManager) ? (formData.packageManager as PackageManager) : (availableManagers[0] as PackageManager) - - setFormData((prev) => ({ - ...prev, - osType: newOs, - packageManager: newManager, - })) + setFormData((prev) => ({ ...prev, osType: newOs, packageManager: newManager })) } + const inputClass = + 'w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary' + if (!isOpen) return null return (
-
-
-

+
+
+

{editingPackage ? 'Paket bearbeiten' : 'Neues Paket'}

-
- {/* Name */}
- + setFormData({ ...formData, name: e.target.value })} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + className={inputClass} />
- {/* Version */}
- + setFormData({ ...formData, version: e.target.value })} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + className={inputClass} />
- {/* OS */}
- +
- {/* Package Manager */}
- +
- {/* Package Name */}
- + setFormData({ ...formData, packageName: e.target.value })} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + className={inputClass} />
- {/* Installer URL (nur für "direct") */} {formData.packageManager === 'direct' && ( -
- - setFormData({ ...formData, installerUrl: e.target.value || undefined })} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
+ <> +
+ + + setFormData({ ...formData, installerUrl: e.target.value || undefined }) + } + className={inputClass} + /> +
+
+ + + setFormData({ ...formData, checksum: e.target.value || undefined }) + } + className={cn(inputClass, 'font-mono text-xs')} + /> +
+
+ + + setFormData({ ...formData, silentArgs: e.target.value || undefined }) + } + className={inputClass} + /> +
+ )} - {/* Checksum (nur für "direct") */} - {formData.packageManager === 'direct' && ( -
- - setFormData({ ...formData, checksum: e.target.value || undefined })} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-xs" - /> -
- )} - - {/* Silent Args (nur für "direct") */} - {formData.packageManager === 'direct' && ( -
- - setFormData({ ...formData, silentArgs: e.target.value || undefined })} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
- )} - - {/* Buttons */}
@@ -475,7 +466,7 @@ function CreatePackageModal({ interface DeployTabProps { packages: SoftwarePackage[] - deployMutation: any + deployMutation: ReturnType> } function DeployTab({ packages, deployMutation }: DeployTabProps) { @@ -483,28 +474,26 @@ function DeployTab({ packages, deployMutation }: DeployTabProps) { const [agentId, setAgentId] = useState('') const [action, setAction] = useState<'install' | 'uninstall'>('install') + const inputClass = + 'w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary' + const handleDeploy = (e: React.FormEvent) => { e.preventDefault() if (!selectedPackageId || !agentId) return - deployMutation.mutate({ - agentId, - packageId: selectedPackageId as number, - action, - }) + deployMutation.mutate({ agentId, packageId: selectedPackageId as number, action }) } return (
-
+
- {/* Package Selection */}
- +
- {/* Agent ID */}
- + setAgentId(e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm" + className={cn(inputClass, 'font-mono')} /> -

+

Finde die Agent-ID im Agent-Dashboard oder Agent-Details

- {/* Action */}
- +
- - + {(['install', 'uninstall'] as const).map((a) => ( + + ))}
- {/* Submit */} - {/* Info */} -
+

Hinweis:

-

+

Die Task wird erstellt und der Agent führt sie beim nächsten Heartbeat aus (ca. 1-2 Minuten). Überwache den Task-Fortschritt im Agent-Dashboard.