Răsfoiți Sursa

Added CPU / Drive webui Modals

Tim Jones 1 lună în urmă
părinte
comite
340b25a29e
54 a modificat fișierele cu 2020 adăugiri și 314 ștergeri
  1. BIN
      .DS_Store
  2. 848 168
      Commands.md
  3. 4 15
      RackPeek.Domain/Helpers/ThrowIfInvalid.cs
  4. 2 0
      RackPeek.Domain/Resources/Hardware/AccessPoints/UpdateAccessPointUseCase.cs
  5. 10 1
      RackPeek.Domain/Resources/Hardware/Desktops/Cpus/AddDesktopCpuUseCase.cs
  6. 3 0
      RackPeek.Domain/Resources/Hardware/Desktops/Cpus/UpdateDesktopCpuUseCase.cs
  7. 3 0
      RackPeek.Domain/Resources/Hardware/Desktops/Drives/AddDesktopDriveUseCase.cs
  8. 3 0
      RackPeek.Domain/Resources/Hardware/Desktops/Drives/UpdateDesktopDriveUseCase.cs
  9. 3 0
      RackPeek.Domain/Resources/Hardware/Desktops/Gpus/AddDesktopGpuUseCase.cs
  10. 3 0
      RackPeek.Domain/Resources/Hardware/Desktops/Gpus/UpdateDesktopGpuUseCase.cs
  11. 3 0
      RackPeek.Domain/Resources/Hardware/Desktops/Nics/AddDesktopNicUseCase.cs
  12. 3 0
      RackPeek.Domain/Resources/Hardware/Desktops/Nics/UpdateDesktopNicUseCase.cs
  13. 2 0
      RackPeek.Domain/Resources/Hardware/Desktops/UpdateDesktopUseCase.cs
  14. 2 0
      RackPeek.Domain/Resources/Hardware/Firewalls/UpdateFirewallUseCase.cs
  15. 3 0
      RackPeek.Domain/Resources/Hardware/Laptops/Cpus/AddDesktopCpuUseCase.cs
  16. 3 0
      RackPeek.Domain/Resources/Hardware/Laptops/Cpus/UpdateDesktopCpuUseCase.cs
  17. 3 0
      RackPeek.Domain/Resources/Hardware/Laptops/Drives/AddDesktopDriveUseCase.cs
  18. 3 0
      RackPeek.Domain/Resources/Hardware/Laptops/Drives/UpdateDesktopDriveUseCase.cs
  19. 3 0
      RackPeek.Domain/Resources/Hardware/Laptops/Gpus/AddDesktopGpuUseCase.cs
  20. 3 0
      RackPeek.Domain/Resources/Hardware/Laptops/Gpus/UpdateDesktopGpuUseCase.cs
  21. 13 0
      RackPeek.Domain/Resources/Hardware/Models/Drive.cs
  22. 3 0
      RackPeek.Domain/Resources/Hardware/Routers/UpdateRouterUseCase.cs
  23. 6 3
      RackPeek.Domain/Resources/Hardware/Servers/Cpus/AddCpuUseCase.cs
  24. 6 3
      RackPeek.Domain/Resources/Hardware/Servers/Cpus/UpdateCpuUseCase.cs
  25. 7 3
      RackPeek.Domain/Resources/Hardware/Servers/Drives/AddDriveUseCase.cs
  26. 4 1
      RackPeek.Domain/Resources/Hardware/Servers/Drives/RemoveDriveUseCase.cs
  27. 8 2
      RackPeek.Domain/Resources/Hardware/Servers/Drives/UpdateDriveUseCase.cs
  28. 3 0
      RackPeek.Domain/Resources/Hardware/Servers/Gpus/AddGpuUseCase.cs
  29. 3 0
      RackPeek.Domain/Resources/Hardware/Servers/Gpus/UpdateGpuUseCase.cs
  30. 3 0
      RackPeek.Domain/Resources/Hardware/Servers/Nics/AddNicUseCase.cs
  31. 3 0
      RackPeek.Domain/Resources/Hardware/Servers/Nics/UpdateNicUseCase.cs
  32. 10 0
      RackPeek.Domain/Resources/Hardware/Servers/UpdateServerUseCase.cs
  33. 3 0
      RackPeek.Domain/Resources/Hardware/Switches/UpdateSwitchUseCase.cs
  34. 3 0
      RackPeek.Domain/Resources/Hardware/UpsUnits/UpdateUpsUseCase.cs
  35. 3 0
      RackPeek.Domain/Resources/Services/UseCases/UpdateServiceUseCase.cs
  36. 3 0
      RackPeek.Domain/Resources/SystemResources/UseCases/AddSystemDriveUseCase.cs
  37. 3 0
      RackPeek.Domain/Resources/SystemResources/UseCases/UpdateSystemDriveUseCase.cs
  38. 3 0
      RackPeek.Domain/Resources/SystemResources/UseCases/UpdateSystemUseCase.cs
  39. 1 0
      RackPeek.Web/Components/Hardware/HardwareDetailsPage.razor
  40. 164 0
      RackPeek.Web/Components/Modals/CpuModal.razor
  41. 163 0
      RackPeek.Web/Components/Modals/DriveModal.razor
  42. 136 0
      RackPeek.Web/Components/Modals/RamModal.razor
  43. 136 0
      RackPeek.Web/Components/Modals/StringValueModal.razor
  44. 228 25
      RackPeek.Web/Components/Servers/ServerCardComponent.razor
  45. 3 1
      RackPeek.Web/Program.cs
  46. 0 7
      RackPeek.Web/RackPeek.Web.csproj
  47. 1 8
      RackPeek/Commands/Desktops/Cpus/DesktopCpuAddCommand.cs
  48. 2 0
      RackPeek/Commands/Servers/ServerSetCommand.cs
  49. 168 65
      RackPeek/Yaml/YamlResourceCollection.cs
  50. 1 0
      Tests/EndToEnd/ServiceYamlE2ETests.cs
  51. 7 4
      Tests/HardwareResources/AddDriveUseCaseTests.cs
  52. 5 2
      Tests/HardwareResources/RemoveDriveUseCaseTests.cs
  53. 9 6
      Tests/HardwareResources/UpdateDriveUseCaseTests.cs
  54. 2 0
      Tests/HardwareResources/UpdateServerUseCaseTests.cs

BIN
.DS_Store


Fișier diff suprimat deoarece este prea mare
+ 848 - 168
Commands.md


+ 4 - 15
RackPeek.Domain/Helpers/ThrowIfInvalid.cs

@@ -1,4 +1,5 @@
 using System.ComponentModel.DataAnnotations;
+using RackPeek.Domain.Resources.Hardware.Models;
 
 namespace RackPeek.Domain.Helpers;
 
@@ -113,26 +114,14 @@ public static class ThrowIfInvalid
     #endregion
 
     #region Drives
-
-    public static readonly string[] ValidDriveTypes =
-    {
-        // Flash storage
-        "nvme", "ssd",
-        // Traditional spinning disks
-        "hdd",
-        // Enterprise interfaces
-        "sas", "sata",
-        // External / removable
-        "usb", "sdcard", "micro-sd"
-    };
-
+    
     public static void DriveType(string driveType)
     {
         if (string.IsNullOrWhiteSpace(driveType)) throw new ValidationException("Drive type is required.");
 
         var normalized = driveType.Trim().ToLowerInvariant();
 
-        if (ValidDriveTypes.Contains(normalized)) return;
+        if (Drive.ValidDriveTypes.Contains(normalized)) return;
 
         var suggestions = GetDriveTypeSuggestions(normalized).ToList();
 
@@ -145,7 +134,7 @@ public static class ThrowIfInvalid
 
     private static IEnumerable<string> GetDriveTypeSuggestions(string input)
     {
-        return ValidDriveTypes.Select(type => new { Type = type, Score = SimilarityScore(input, type) })
+        return Drive.ValidDriveTypes.Select(type => new { Type = type, Score = SimilarityScore(input, type) })
             .Where(x => x.Score >= 0.5)
             .OrderByDescending(x => x.Score)
             .Take(3)

+ 2 - 0
RackPeek.Domain/Resources/Hardware/AccessPoints/UpdateAccessPointUseCase.cs

@@ -11,6 +11,8 @@ public class UpdateAccessPointUseCase(IHardwareRepository repository) : IUseCase
         double? speed = null
     )
     {
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
         var ap = await repository.GetByNameAsync(name) as AccessPoint;

+ 10 - 1
RackPeek.Domain/Resources/Hardware/Desktops/Cpus/AddDesktopCpuUseCase.cs

@@ -5,10 +5,19 @@ namespace RackPeek.Domain.Resources.Hardware.Desktops.Cpus;
 
 public class AddDesktopCpuUseCase(IHardwareRepository repository) : IUseCase
 {
-    public async Task ExecuteAsync(string name, Cpu cpu)
+    public async Task ExecuteAsync(string name, string? model, int? cores, int? threads)
     {
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
+        
+        // ToDo validate / normalize all inputs
+        
+        var cpu = new Cpu
+        {
+            Model = model,
+            Cores = cores,
+            Threads = threads
+        };
 
         var desktop = await repository.GetByNameAsync(name) as Desktop
                       ?? throw new InvalidOperationException($"Desktop '{name}' not found.");

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Desktops/Cpus/UpdateDesktopCpuUseCase.cs

@@ -7,6 +7,9 @@ public class UpdateDesktopCpuUseCase(IHardwareRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(string name, int index, Cpu updated)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Desktops/Drives/AddDesktopDriveUseCase.cs

@@ -7,6 +7,9 @@ public class AddDesktopDriveUseCase(IHardwareRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(string name, Drive drive)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Desktops/Drives/UpdateDesktopDriveUseCase.cs

@@ -7,6 +7,9 @@ public class UpdateDesktopDriveUseCase(IHardwareRepository repository) : IUseCas
 {
     public async Task ExecuteAsync(string name, int index, Drive updated)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Desktops/Gpus/AddDesktopGpuUseCase.cs

@@ -7,6 +7,9 @@ public class AddDesktopGpuUseCase(IHardwareRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(string name, Gpu gpu)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Desktops/Gpus/UpdateDesktopGpuUseCase.cs

@@ -7,6 +7,9 @@ public class UpdateDesktopGpuUseCase(IHardwareRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(string name, int index, Gpu updated)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Desktops/Nics/AddDesktopNicUseCase.cs

@@ -7,6 +7,9 @@ public class AddDesktopNicUseCase(IHardwareRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(string name, Nic nic)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Desktops/Nics/UpdateDesktopNicUseCase.cs

@@ -7,6 +7,9 @@ public class UpdateDesktopNicUseCase(IHardwareRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(string name, int index, Nic updated)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 2 - 0
RackPeek.Domain/Resources/Hardware/Desktops/UpdateDesktopUseCase.cs

@@ -10,6 +10,8 @@ public class UpdateDesktopUseCase(IHardwareRepository repository) : IUseCase
         string? model = null
     )
     {
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 2 - 0
RackPeek.Domain/Resources/Hardware/Firewalls/UpdateFirewallUseCase.cs

@@ -12,6 +12,8 @@ public class UpdateFirewallUseCase(IHardwareRepository repository) : IUseCase
         bool? poe = null
     )
     {
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Laptops/Cpus/AddDesktopCpuUseCase.cs

@@ -7,6 +7,9 @@ public class AddLaptopCpuUseCase(IHardwareRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(string name, Cpu cpu)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
         var laptop = await repository.GetByNameAsync(name) as Laptop

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Laptops/Cpus/UpdateDesktopCpuUseCase.cs

@@ -7,6 +7,9 @@ public class UpdateLaptopCpuUseCase(IHardwareRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(string name, int index, Cpu updated)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
         var laptop = await repository.GetByNameAsync(name) as Laptop

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Laptops/Drives/AddDesktopDriveUseCase.cs

@@ -7,6 +7,9 @@ public class AddLaptopDriveUseCase(IHardwareRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(string name, Drive drive)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Laptops/Drives/UpdateDesktopDriveUseCase.cs

@@ -7,6 +7,9 @@ public class UpdateLaptopDriveUseCase(IHardwareRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(string name, int index, Drive updated)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Laptops/Gpus/AddDesktopGpuUseCase.cs

@@ -7,6 +7,9 @@ public class AddLaptopGpuUseCase(IHardwareRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(string name, Gpu gpu)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
         var laptop = await repository.GetByNameAsync(name) as Laptop

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Laptops/Gpus/UpdateDesktopGpuUseCase.cs

@@ -7,6 +7,9 @@ public class UpdateLaptopGpuUseCase(IHardwareRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(string name, int index, Gpu updated)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 13 - 0
RackPeek.Domain/Resources/Hardware/Models/Drive.cs

@@ -4,4 +4,17 @@ public class Drive
 {
     public string? Type { get; set; }
     public int? Size { get; set; }
+    
+    public static readonly string[] ValidDriveTypes =
+    {
+        // Flash storage
+        "nvme", "ssd",
+        // Traditional spinning disks
+        "hdd",
+        // Enterprise interfaces
+        "sas", "sata",
+        // External / removable
+        "usb", "sdcard", "micro-sd"
+    };
+    
 }

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Routers/UpdateRouterUseCase.cs

@@ -12,6 +12,9 @@ public class UpdateRouterUseCase(IHardwareRepository repository) : IUseCase
         bool? poe = null
     )
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 6 - 3
RackPeek.Domain/Resources/Hardware/Servers/Cpus/AddCpuUseCase.cs

@@ -7,10 +7,13 @@ public class AddCpuUseCase(IHardwareRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(
         string name,
-        string model,
-        int cores,
-        int threads)
+        string? model,
+        int? cores,
+        int? threads)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 6 - 3
RackPeek.Domain/Resources/Hardware/Servers/Cpus/UpdateCpuUseCase.cs

@@ -8,10 +8,13 @@ public class UpdateCpuUseCase(IHardwareRepository repository) : IUseCase
     public async Task ExecuteAsync(
         string name,
         int index,
-        string model,
-        int cores,
-        int threads)
+        string? model,
+        int? cores,
+        int? threads)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 7 - 3
RackPeek.Domain/Resources/Hardware/Servers/Drives/AddDriveUseCase.cs

@@ -7,15 +7,19 @@ public class AddDrivesUseCase(IHardwareRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(
         string name,
-        string type,
-        int size)
+        string? type,
+        int? size)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
         var hardware = await repository.GetByNameAsync(name);
 
-        if (hardware is not Server server) return;
+        if (hardware is not Server server)
+            throw new NotFoundException($"Server '{name}' not found.");
 
         server.Drives ??= [];
 

+ 4 - 1
RackPeek.Domain/Resources/Hardware/Servers/Drives/RemoveDriveUseCase.cs

@@ -11,10 +11,13 @@ public class RemoveDriveUseCase(IHardwareRepository repository) : IUseCase
         ThrowIfInvalid.ResourceName(name);
 
         var hardware = await repository.GetByNameAsync(name);
-        if (hardware is not Server server) return;
+        if (hardware is not Server server)
+            throw new NotFoundException($"Server '{name}' not found.");
+
         server.Drives ??= [];
         if (index < 0 || index >= server.Drives.Count)
             throw new ArgumentOutOfRangeException(nameof(index), "Drive index out of range.");
+        
         server.Drives.RemoveAt(index);
         await repository.UpdateAsync(server);
     }

+ 8 - 2
RackPeek.Domain/Resources/Hardware/Servers/Drives/UpdateDriveUseCase.cs

@@ -5,17 +5,23 @@ namespace RackPeek.Domain.Resources.Hardware.Servers.Drives;
 
 public class UpdateDriveUseCase(IHardwareRepository repository) : IUseCase
 {
-    public async Task ExecuteAsync(string name, int index, string type, int size)
+    public async Task ExecuteAsync(string name, int index, string? type, int? size)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
         var hardware = await repository.GetByNameAsync(name);
-        if (hardware is not Server server) return;
+        if (hardware is not Server server)
+            throw new NotFoundException($"Server '{name}' not found.");
+
 
         server.Drives ??= [];
         if (index < 0 || index >= server.Drives.Count)
             throw new ArgumentOutOfRangeException(nameof(index), "Drive index out of range.");
+        
         var drive = server.Drives[index];
         drive.Type = type;
         drive.Size = size;

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Servers/Gpus/AddGpuUseCase.cs

@@ -10,6 +10,9 @@ public class AddGpuUseCase(IHardwareRepository repository) : IUseCase
         string model,
         int vram)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Servers/Gpus/UpdateGpuUseCase.cs

@@ -11,6 +11,9 @@ public class UpdateGpuUseCase(IHardwareRepository repository) : IUseCase
         string model,
         int vram)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Servers/Nics/AddNicUseCase.cs

@@ -11,6 +11,9 @@ public class AddNicUseCase(IHardwareRepository repository) : IUseCase
         int speed,
         int ports)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
         ThrowIfInvalid.NicSpeed(speed);

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Servers/Nics/UpdateNicUseCase.cs

@@ -13,6 +13,9 @@ public class UpdateNicUseCase(IHardwareRepository repository) : IUseCase
         int speed,
         int ports)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
         ThrowIfInvalid.NicSpeed(speed);

+ 10 - 0
RackPeek.Domain/Resources/Hardware/Servers/UpdateServerUseCase.cs

@@ -8,9 +8,13 @@ public class UpdateServerUseCase(IHardwareRepository repository) : IUseCase
     public async Task ExecuteAsync(
         string name,
         int? ramGb = null,
+        int? ramMts = null,
         bool? ipmi = null
     )
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
@@ -25,6 +29,12 @@ public class UpdateServerUseCase(IHardwareRepository repository) : IUseCase
             server.Ram ??= new Ram();
             server.Ram.Size = ramGb.Value;
         }
+        
+        if (ramMts.HasValue)
+        {
+            server.Ram ??= new Ram();
+            server.Ram.Mts = ramMts.Value;
+        }
 
         // ---- IPMI ----
         if (ipmi.HasValue) server.Ipmi = ipmi.Value;

+ 3 - 0
RackPeek.Domain/Resources/Hardware/Switches/UpdateSwitchUseCase.cs

@@ -12,6 +12,9 @@ public class UpdateSwitchUseCase(IHardwareRepository repository) : IUseCase
         bool? poe = null
     )
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 3 - 0
RackPeek.Domain/Resources/Hardware/UpsUnits/UpdateUpsUseCase.cs

@@ -11,6 +11,9 @@ public class UpdateUpsUseCase(IHardwareRepository repository) : IUseCase
         int? va = null
     )
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 

+ 3 - 0
RackPeek.Domain/Resources/Services/UseCases/UpdateServiceUseCase.cs

@@ -14,6 +14,9 @@ public class UpdateServiceUseCase(IServiceRepository repository, ISystemReposito
         string? runsOn = null
     )
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.ServiceName(name);
         ThrowIfInvalid.ResourceName(name);
         var service = await repository.GetByNameAsync(name);

+ 3 - 0
RackPeek.Domain/Resources/SystemResources/UseCases/AddSystemDriveUseCase.cs

@@ -9,6 +9,9 @@ public class AddSystemDriveUseCase(ISystemRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(string systemName, string driveType, int size)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         ThrowIfInvalid.ResourceName(systemName);
         
         var driveTypeNormalized = Normalize.DriveType(driveType);

+ 3 - 0
RackPeek.Domain/Resources/SystemResources/UseCases/UpdateSystemDriveUseCase.cs

@@ -9,6 +9,9 @@ public class UpdateSystemDriveUseCase(ISystemRepository repository) : IUseCase
 {
     public async Task ExecuteAsync(string systemName, int index, string driveType, int size)
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         ThrowIfInvalid.ResourceName(systemName);
         var driveTypeNormalized = Normalize.DriveType(driveType);
         ThrowIfInvalid.DriveType(driveTypeNormalized);

+ 3 - 0
RackPeek.Domain/Resources/SystemResources/UseCases/UpdateSystemUseCase.cs

@@ -14,6 +14,9 @@ public class UpdateSystemUseCase(ISystemRepository repository, IHardwareReposito
         string? runsOn = null
     )
     {
+        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
+        // ToDo validate / normalize all inputs
+        
         name = Normalize.SystemName(name);
         ThrowIfInvalid.ResourceName(name);
         var system = await repository.GetByNameAsync(name);

+ 1 - 0
RackPeek.Web/Components/Hardware/HardwareDetailsPage.razor

@@ -5,6 +5,7 @@
 @using RackPeek.Web.Components.Components
 @using RackPeek.Web.Components.Desktops
 @using RackPeek.Web.Components.Switches
+@using RackPeek.Web.Components.Servers
 @inject IHardwareRepository HardwareRepository
 @inject GetHardwareSystemTreeUseCase GetHardwareSystemTreeUseCase
 <PageTitle>Hardware Details</PageTitle>

+ 164 - 0
RackPeek.Web/Components/Modals/CpuModal.razor

@@ -0,0 +1,164 @@
+@using System.ComponentModel.DataAnnotations
+@using RackPeek.Domain.Resources.Hardware.Models
+
+@if (IsOpen)
+{
+    <div class="fixed inset-0 z-50 flex items-center justify-center">
+        <!-- Backdrop -->
+        <div class="absolute inset-0 bg-black/70" @onclick="Cancel"></div>
+
+        <!-- Modal -->
+        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4">
+            <!-- Header -->
+            <div class="flex justify-between items-center mb-4">
+                <div class="text-zinc-100 text-sm font-medium">
+                    @(IsEdit ? "Modify CPU" : "Add CPU")
+                </div>
+
+                <button
+                    class="text-zinc-400 hover:text-zinc-200"
+                    @onclick="Cancel">
+                    ✕
+                </button>
+            </div>
+
+            <!-- Form -->
+            <EditForm Model="_model" OnValidSubmit="HandleValidSubmit">
+                <DataAnnotationsValidator />
+                <ValidationSummary class="text-xs text-red-400 mb-3" />
+
+                <div class="space-y-3 text-sm">
+                    <div>
+                        <label class="block text-zinc-400 mb-1">Model</label>
+                        <InputText
+                            class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                            @bind-Value="_model.Model" />
+                    </div>
+
+                    <div class="grid grid-cols-2 gap-3">
+                        <div>
+                            <label class="block text-zinc-400 mb-1">Cores</label>
+                            <InputNumber
+                                class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                @bind-Value="_model.Cores" />
+                        </div>
+
+                        <div>
+                            <label class="block text-zinc-400 mb-1">Threads</label>
+                            <InputNumber
+                                class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                @bind-Value="_model.Threads" />
+                        </div>
+                    </div>
+                </div>
+
+                <!-- Actions -->
+                <div class="flex justify-between items-center mt-5">
+                    @if (IsEdit)
+                    {
+                        <button
+                            type="button"
+                            class="text-red-400 hover:text-red-300 text-sm"
+                            @onclick="HandleDelete">
+                            Delete CPU
+                        </button>
+                    }
+                    else
+                    {
+                        <span></span>
+                    }
+
+                    <div class="flex gap-2">
+                        <button
+                            type="button"
+                            class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
+                            @onclick="Cancel">
+                            Cancel
+                        </button>
+
+                        <button
+                            type="submit"
+                            class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500">
+                            @(IsEdit ? "Update" : "Add")
+                        </button>
+                    </div>
+                </div>
+            </EditForm>
+        </div>
+    </div>
+}
+@code {
+    [Parameter] public bool IsOpen { get; set; }
+    [Parameter] public EventCallback<bool> IsOpenChanged { get; set; }
+
+    [Parameter] public Cpu? Value { get; set; }
+
+    [Parameter] public EventCallback<Cpu> OnSubmit { get; set; }
+
+    // NEW: delete callback
+    [Parameter] public EventCallback<Cpu> OnDelete { get; set; }
+
+    private CpuFormModel _model = new();
+
+    private bool IsEdit => Value is not null;
+
+    protected override void OnParametersSet()
+    {
+        if (IsOpen)
+        {
+            _model = Value is null
+                ? new CpuFormModel()
+                : new CpuFormModel
+                {
+                    Model = Value.Model,
+                    Cores = Value.Cores,
+                    Threads = Value.Threads
+                };
+        }
+    }
+
+    private async Task HandleValidSubmit()
+    {
+        var cpu = new Cpu
+        {
+            Model = _model.Model,
+            Cores = _model.Cores,
+            Threads = _model.Threads
+        };
+
+        await OnSubmit.InvokeAsync(cpu);
+        await Close();
+    }
+
+    private async Task HandleDelete()
+    {
+        if (Value is not null)
+        {
+            await OnDelete.InvokeAsync(Value);
+            await Close();
+        }
+    }
+
+    private async Task Cancel()
+    {
+        await Close();
+    }
+
+    private async Task Close()
+    {
+        _model = new();
+        await IsOpenChanged.InvokeAsync(false);
+    }
+
+    private class CpuFormModel
+    {
+        [Required]
+        public string? Model { get; set; }
+
+        [Range(1, 1024)]
+        public int? Cores { get; set; }
+
+        [Range(1, 2048)]
+        public int? Threads { get; set; }
+    }
+}

+ 163 - 0
RackPeek.Web/Components/Modals/DriveModal.razor

@@ -0,0 +1,163 @@
+@using System.ComponentModel.DataAnnotations
+@using RackPeek.Domain.Resources.Hardware.Models
+
+@if (IsOpen)
+{
+    <div class="fixed inset-0 z-50 flex items-center justify-center">
+        <!-- Backdrop -->
+        <div class="absolute inset-0 bg-black/70" @onclick="Cancel"></div>
+
+        <!-- Modal -->
+        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4">
+            <!-- Header -->
+            <div class="flex justify-between items-center mb-4">
+                <div class="text-zinc-100 text-sm font-medium">
+                    @(IsEdit ? "Modify Drive" : "Add Drive")
+                </div>
+
+                <button
+                    class="text-zinc-400 hover:text-zinc-200"
+                    @onclick="Cancel">
+                    ✕
+                </button>
+            </div>
+
+            <!-- Form -->
+            <EditForm Model="_model" OnValidSubmit="HandleValidSubmit">
+                <DataAnnotationsValidator />
+                <ValidationSummary class="text-xs text-red-400 mb-3" />
+
+                <div class="space-y-3 text-sm">
+                    <div>
+                        <label class="block text-zinc-400 mb-1">
+                            Type
+                        </label>
+
+                        <InputSelect
+                            class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                            @bind-Value="_model.Type">
+                            <option value="">Select type</option>
+
+                            @foreach (var type in Drive.ValidDriveTypes)
+                            {
+                                <option value="@type">@type</option>
+                            }
+                        </InputSelect>
+                    </div>
+
+                    <div>
+                        <label class="block text-zinc-400 mb-1">
+                            Size (GB)
+                        </label>
+
+                        <InputNumber
+                            class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                            @bind-Value="_model.Size" />
+                    </div>
+                </div>
+
+                <!-- Actions -->
+                <div class="flex justify-between items-center mt-5">
+                    @if (IsEdit)
+                    {
+                        <button
+                            type="button"
+                            class="text-red-400 hover:text-red-300 text-sm"
+                            @onclick="HandleDelete">
+                            Delete Drive
+                        </button>
+                    }
+                    else
+                    {
+                        <span></span>
+                    }
+
+                    <div class="flex gap-2">
+                        <button
+                            type="button"
+                            class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
+                            @onclick="Cancel">
+                            Cancel
+                        </button>
+
+                        <button
+                            type="submit"
+                            class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500">
+                            @(IsEdit ? "Update" : "Add")
+                        </button>
+                    </div>
+                </div>
+            </EditForm>
+        </div>
+    </div>
+}
+
+@code {
+    [Parameter] public bool IsOpen { get; set; }
+    [Parameter] public EventCallback<bool> IsOpenChanged { get; set; }
+
+    [Parameter] public Drive? Value { get; set; }
+
+    [Parameter] public EventCallback<Drive> OnSubmit { get; set; }
+
+    [Parameter] public EventCallback<Drive> OnDelete { get; set; }
+
+    private DriveFormModel _model = new();
+
+    private bool IsEdit => Value is not null;
+
+    protected override void OnParametersSet()
+    {
+        if (IsOpen)
+        {
+            _model = Value is null
+                ? new DriveFormModel()
+                : new DriveFormModel
+                {
+                    Type = Value.Type,
+                    Size = Value.Size
+                };
+        }
+    }
+
+    private async Task HandleValidSubmit()
+    {
+        var drive = new Drive
+        {
+            Type = _model.Type,
+            Size = _model.Size
+        };
+
+        await OnSubmit.InvokeAsync(drive);
+        await Close();
+    }
+
+    private async Task HandleDelete()
+    {
+        if (Value is not null)
+        {
+            await OnDelete.InvokeAsync(Value);
+            await Close();
+        }
+    }
+
+    private async Task Cancel()
+    {
+        await Close();
+    }
+
+    private async Task Close()
+    {
+        _model = new();
+        await IsOpenChanged.InvokeAsync(false);
+    }
+
+    private class DriveFormModel
+    {
+        [Required]
+        public string? Type { get; set; }
+
+        [Range(1, 1024 * 1024)]
+        public int? Size { get; set; }
+    }
+}

+ 136 - 0
RackPeek.Web/Components/Modals/RamModal.razor

@@ -0,0 +1,136 @@
+@using System.ComponentModel.DataAnnotations
+@using RackPeek.Domain.Resources.Hardware.Models
+
+@if (IsOpen)
+{
+    <div class="fixed inset-0 z-50 flex items-center justify-center">
+        <!-- Backdrop -->
+        <div class="absolute inset-0 bg-black/70" @onclick="Cancel"></div>
+
+        <!-- Modal -->
+        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4">
+            <!-- Header -->
+            <div class="flex justify-between items-center mb-4">
+                <div class="text-zinc-100 text-sm font-medium">
+                    @(IsEdit ? "Modify RAM" : "Add RAM")
+                </div>
+
+                <button
+                    class="text-zinc-400 hover:text-zinc-200"
+                    @onclick="Cancel">
+                    ✕
+                </button>
+            </div>
+
+            <!-- Form -->
+            <EditForm Model="_model" OnValidSubmit="HandleValidSubmit">
+                <DataAnnotationsValidator />
+                <ValidationSummary class="text-xs text-red-400 mb-3" />
+
+                <div class="space-y-3 text-sm">
+                    <div class="grid grid-cols-2 gap-3">
+                        <div>
+                            <label class="block text-zinc-400 mb-1">
+                                Size (GB)
+                            </label>
+                            <InputNumber
+                                class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                @bind-Value="_model.Size" />
+                        </div>
+
+                        <div>
+                            <label class="block text-zinc-400 mb-1">
+                                Speed (MT/s)
+                            </label>
+                            <InputNumber
+                                class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                @bind-Value="_model.Mts" />
+                        </div>
+                    </div>
+                </div>
+
+                <!-- Actions -->
+                <div class="flex justify-between items-center mt-5">
+                    <span></span>
+
+
+                    <div class="flex gap-2">
+                        <button
+                            type="button"
+                            class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
+                            @onclick="Cancel">
+                            Cancel
+                        </button>
+
+                        <button
+                            type="submit"
+                            class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500">
+                            @(IsEdit ? "Update" : "Add")
+                        </button>
+                    </div>
+                </div>
+            </EditForm>
+        </div>
+    </div>
+}
+
+@code {
+    [Parameter] public bool IsOpen { get; set; }
+    [Parameter] public EventCallback<bool> IsOpenChanged { get; set; }
+
+    [Parameter] public Ram? Value { get; set; }
+
+    [Parameter] public EventCallback<Ram> OnSubmit { get; set; }
+    
+    private RamFormModel _model = new();
+
+    private bool IsEdit => Value is not null;
+
+    protected override void OnParametersSet()
+    {
+        if (IsOpen)
+        {
+            _model = Value is null
+                ? new RamFormModel()
+                : new RamFormModel
+                {
+                    Size = Value.Size,
+                    Mts = Value.Mts
+                };
+        }
+    }
+
+    private async Task HandleValidSubmit()
+    {
+        var ram = new Ram
+        {
+            Size = _model.Size,
+            Mts = _model.Mts
+        };
+
+        await OnSubmit.InvokeAsync(ram);
+        await Close();
+    }
+    
+    private async Task Cancel()
+    {
+        await Close();
+    }
+
+    private async Task Close()
+    {
+        _model = new();
+        await IsOpenChanged.InvokeAsync(false);
+    }
+
+    private class RamFormModel
+    {
+        // Both are intentionally nullable and optional
+        [Range(1, 1024)]
+        public int? Size { get; set; }
+
+        [Range(1, 10000)]
+        public int? Mts { get; set; }
+    }
+}
+

+ 136 - 0
RackPeek.Web/Components/Modals/StringValueModal.razor

@@ -0,0 +1,136 @@
+@using System.ComponentModel.DataAnnotations
+
+@if (IsOpen)
+{
+    <div class="fixed inset-0 z-50 flex items-center justify-center">
+        <!-- Backdrop -->
+        <div class="absolute inset-0 bg-black/70" @onclick="Cancel"></div>
+
+        <!-- Modal -->
+        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4">
+            <!-- Header -->
+            <div class="flex justify-between items-center mb-3">
+                <div class="text-zinc-100 text-sm font-medium">
+                    @Title
+                </div>
+
+                <button
+                    class="text-zinc-400 hover:text-zinc-200"
+                    @onclick="Cancel">
+                    ✕
+                </button>
+            </div>
+
+            @if (!string.IsNullOrWhiteSpace(Description))
+            {
+                <div class="text-xs text-zinc-400 mb-4">
+                    @Description
+                </div>
+            }
+
+            <!-- Form -->
+            <EditForm Model="_model" OnValidSubmit="HandleValidSubmit">
+                <DataAnnotationsValidator />
+
+                @if (!string.IsNullOrEmpty(_error))
+                {
+                    <div class="text-xs text-red-400 mb-3">
+                        @_error
+                    </div>
+                }
+
+                <div class="text-sm">
+                    <label class="block text-zinc-400 mb-1">
+                        @Label
+                    </label>
+
+                    <InputText
+                        class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                        @bind-Value="_model.Value" />
+                </div>
+
+                <!-- Actions -->
+                <div class="flex justify-end gap-2 mt-5">
+                    <button
+                        type="button"
+                        class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
+                        @onclick="Cancel">
+                        Cancel
+                    </button>
+
+                    <button
+                        type="submit"
+                        class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500">
+                        Accept
+                    </button>
+                </div>
+            </EditForm>
+        </div>
+    </div>
+}
+
+@code {
+    [Parameter] public bool IsOpen { get; set; }
+    [Parameter] public EventCallback<bool> IsOpenChanged { get; set; }
+
+    [Parameter] public string Title { get; set; } = "Edit value";
+    [Parameter] public string? Description { get; set; }
+    [Parameter] public string Label { get; set; } = "Value";
+
+    [Parameter] public string? Value { get; set; }
+
+    /// <summary>
+    /// Called when Accept is clicked.
+    /// May throw an exception (e.g. validation error).
+    /// </summary>
+    [Parameter] public EventCallback<string> OnSubmit { get; set; }
+
+    private FormModel _model = new();
+    private string? _error;
+
+    protected override void OnParametersSet()
+    {
+        if (IsOpen)
+        {
+            _error = null;
+            _model = new FormModel
+            {
+                Value = Value
+            };
+        }
+    }
+
+    private async Task HandleValidSubmit()
+    {
+        _error = null;
+
+        try
+        {
+            await OnSubmit.InvokeAsync(_model.Value!);
+            await Close();
+        }
+        catch (Exception ex)
+        {
+            // Show exception message instead of closing
+            _error = ex.Message;
+        }
+    }
+
+    private async Task Cancel()
+    {
+        await Close();
+    }
+
+    private async Task Close()
+    {
+        _model = new();
+        _error = null;
+        await IsOpenChanged.InvokeAsync(false);
+    }
+
+    private class FormModel
+    {
+        [Required]
+        public string? Value { get; set; }
+    }
+}

+ 228 - 25
RackPeek.Web/Components/Servers/ServerCardComponent.razor

@@ -1,4 +1,18 @@
-@typeparam TServer where TServer : RackPeek.Domain.Resources.Hardware.Models.Server
+@using RackPeek.Domain.Resources.Hardware.Models
+@using RackPeek.Domain.Resources.Hardware.Servers
+@using RackPeek.Domain.Resources.Hardware.Servers.Cpus
+@using RackPeek.Domain.Resources.Hardware.Servers.Drives
+@using RackPeek.Web.Components.Modals
+@inject AddCpuUseCase AddCpuUseCase
+@inject RemoveCpuUseCase RemoveCpuUseCase
+@inject UpdateCpuUseCase UpdateCpuUseCase
+
+@inject AddDrivesUseCase AddDriveUseCase
+@inject RemoveDriveUseCase RemoveDriveUseCase
+@inject UpdateDriveUseCase UpdateDriveUseCase
+
+@inject GetServerUseCase GetServerUseCase
+@inject UpdateServerUseCase UpdateServerUseCase
 
 <div class="border border-zinc-800 rounded p-4 bg-zinc-900">
     <div class="flex justify-between items-center mb-3">
@@ -13,43 +27,103 @@
     </div>
 
     <div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
+        
+        <div>
+            <div class="flex items-center justify-between mb-1 group">
+                <div class="text-zinc-400">CPU
+                    <button
+                        class="hover:text-emerald-400 group-hover:opacity-100 transition"
+                        title="Add CPU"
+                        @onclick="OpenAddCpu">
+                        +
+                    </button>
+                </div>
+            </div>
 
-        @if (Server.Cpus?.Any() == true)
-        {
-            <div>
-                <div class="text-zinc-400 mb-1">CPU</div>
+            @if (Server.Cpus?.Any() == true)
+            {
+                <!-- CPU rows -->
                 @foreach (var cpu in Server.Cpus)
                 {
-                    <div class="text-zinc-300">
-                        @cpu.Model — @cpu.Cores cores / @cpu.Threads threads
+                    <div
+                        class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
+
+                        <div class="flex gap-2 group-hover:opacity-100 transition">
+                            <button
+                                class="hover:text-emerald-400"
+                                title="Edit CPU"
+                                @onclick="() => OpenEditCpu(cpu)">
+                                @cpu.Model — @cpu.Cores cores / @cpu.Threads threads
+                            </button>
+                        </div>
                     </div>
                 }
+            }
+        </div>
+
+
+        <div>
+            <div class="text-zinc-400 mb-1">RAM
+                @if (Server.Ram is null)
+                {
+                    <button
+                        class="hover:text-emerald-400 group-hover:opacity-100 transition"
+                        title="Add RAM"
+                        @onclick="EditRam">
+                        +
+                    </button>
+                }
             </div>
-        }
+            @if (Server.Ram is not null)
+            {
+                
+                <div
+                    class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
+                    <div class="flex gap-2 group-hover:opacity-100 transition">
+                        <button
+                            class="hover:text-emerald-400"
+                            title="Edit RAM"
+                            @onclick="EditRam">
+                            @($"{Server.Ram.Size} GB {@Server.Ram.Mts} MT/s")
+                        </button>
+                    </div>
+                </div>
+            }
+        </div>
 
-        @if (Server.Ram is not null)
-        {
-            <div>
-                <div class="text-zinc-400 mb-1">RAM</div>
-                <div class="text-zinc-300">
-                    @Server.Ram.Size GB @Server.Ram.Mts MT/s
+
+        <div>
+            <div class="flex items-center justify-between mb-1 group">
+                <div class="text-zinc-400">Drives
+                    <button
+                        class="hover:text-emerald-400 group-hover:opacity-100 transition"
+                        title="Add Drive"
+                        @onclick="OpenAddDrive">
+                        +
+                    </button>
                 </div>
             </div>
-        }
 
-        @if (Server.Drives?.Any() == true)
-        {
-            <div>
-                <div class="text-zinc-400 mb-1">Drives</div>
+            @if (Server.Drives?.Any() == true)
+            {
                 @foreach (var drive in Server.Drives)
                 {
-                    <div class="text-zinc-300">
-                        @drive.Type — @drive.Size GB
+                    <div
+                        class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
+
+                        <div class="flex gap-2 group-hover:opacity-100 transition">
+                            <button
+                                class="hover:text-emerald-400"
+                                title="Edit Drive"
+                                @onclick="() => OpenEditDrives(drive)">
+                                @drive.Type — @drive.Size GB
+                            </button>
+                        </div>
                     </div>
                 }
-            </div>
-        }
-
+            }
+        </div>
+            
         @if (Server.Nics?.Any() == true)
         {
             <div>
@@ -79,6 +153,135 @@
     </div>
 </div>
 
+<CpuModal
+    IsOpen="@_cpuModalOpen"
+    IsOpenChanged="v => _cpuModalOpen = v"
+    Value="@_editingCpu"
+    OnSubmit="HandleCpuSubmit" 
+    OnDelete="HandleCpuDelete"/>
+
+<RamModal
+    IsOpen="@_isRamModalOpen"
+    IsOpenChanged="v => _isRamModalOpen = v"
+    Value="@Server.Ram"
+    OnSubmit="HandleRamSubmit"/>
+
+<DriveModal
+    IsOpen="@_driveModalOpen"
+    IsOpenChanged="v => _driveModalOpen = v"
+    Value="@_editingDrive"
+    OnSubmit="HandleDriveSubmit" 
+    OnDelete="HandleDriveDelete"/>
+
 @code {
-    [Parameter] [EditorRequired] public TServer Server { get; set; } = default!;
+    [Parameter] [EditorRequired]
+    public Server Server { get; set; } = default!;
+
+    #region RAM
+    private bool _isRamModalOpen;
+    private void EditRam()
+    {
+        _isRamModalOpen = true;
+    }
+
+    private async Task HandleRamSubmit(Ram value)
+    {
+        _isRamModalOpen = false;
+        await UpdateServerUseCase.ExecuteAsync(Server.Name, value.Size, value.Mts, Server.Ipmi);
+        Server = await GetServerUseCase.ExecuteAsync(Server.Name);
+    }
+    
+    #endregion
+    
+    #region CPU
+    bool _cpuModalOpen;
+    int _editingCpuIndex;
+    Cpu? _editingCpu;
+    
+    void OpenAddCpu()
+    {
+        _editingCpuIndex = -1;
+        _editingCpu = null;
+        _cpuModalOpen = true;
+    }
+    
+    void OpenEditCpu(Cpu cpu)
+    {
+        _editingCpu = cpu;
+        Server.Cpus ??= new();
+        _editingCpuIndex = Server.Cpus.IndexOf(cpu);;
+        _cpuModalOpen = true;
+    }
+    
+    async Task HandleCpuSubmit(Cpu cpu)
+    {
+        Server.Cpus ??= new();
+
+        if (_editingCpuIndex < 0)
+        {
+            await AddCpuUseCase.ExecuteAsync(Server.Name, cpu.Model, cpu.Cores, cpu.Threads);
+        }
+        else
+        {
+            await UpdateCpuUseCase.ExecuteAsync(Server.Name, _editingCpuIndex, cpu.Model, cpu.Cores, cpu.Threads);
+        }
+        
+        Server = await GetServerUseCase.ExecuteAsync(Server.Name);
+    }
+
+    async Task HandleCpuDelete(Cpu cpu)
+    {
+        await RemoveCpuUseCase.ExecuteAsync(Server.Name, _editingCpuIndex);
+        Server = await GetServerUseCase.ExecuteAsync(Server.Name);
+    }
+    
+        
+    #endregion
+    
+    #region Drives
+    bool _driveModalOpen;
+    int _editingDriveIndex;
+    Drive? _editingDrive;
+    
+    void OpenAddDrive()
+    {
+        _editingDriveIndex = -1;
+        _editingDrive = null;
+        _driveModalOpen = true;
+    }
+    
+    void OpenEditDrives(Drive drive)
+    {
+        _editingDrive = drive;
+        Server.Drives ??= new();
+        _editingDriveIndex = Server.Drives.IndexOf(drive);;
+        _driveModalOpen = true;
+    }
+    
+    async Task HandleDriveSubmit(Drive drive)
+    {
+        Server.Drives ??= new();
+
+        if (_editingDriveIndex < 0)
+        {
+            await AddDriveUseCase.ExecuteAsync(Server.Name, drive.Type, drive.Size);
+        }
+        else
+        {
+            await UpdateDriveUseCase.ExecuteAsync(Server.Name, _editingDriveIndex,  drive.Type, drive.Size);
+        }
+        
+        Server = await GetServerUseCase.ExecuteAsync(Server.Name);
+        StateHasChanged();
+    }
+
+    async Task HandleDriveDelete(Drive drive)
+    {
+        await RemoveDriveUseCase.ExecuteAsync(Server.Name, _editingDriveIndex);
+        Server = await GetServerUseCase.ExecuteAsync(Server.Name);
+        StateHasChanged();
+    }
+    
+        
+    #endregion
 }

+ 3 - 1
RackPeek.Web/Program.cs

@@ -1,5 +1,6 @@
 using Microsoft.AspNetCore.Hosting.StaticWebAssets;
 using RackPeek.Domain;
+using RackPeek.Domain.Resources;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Services;
 using RackPeek.Domain.Resources.SystemResources;
@@ -21,7 +22,7 @@ public class Program
 
         var yamlDir = "./config";
 
-        var collection = new YamlResourceCollection(true);
+        var collection = new YamlResourceCollection(false);
         var basePath = Directory.GetCurrentDirectory();
 
         // Resolve yamlDir as relative to basePath
@@ -45,6 +46,7 @@ public class Program
         builder.Services.AddScoped<IHardwareRepository>(_ => new YamlHardwareRepository(collection));
         builder.Services.AddScoped<ISystemRepository>(_ => new YamlSystemRepository(collection));
         builder.Services.AddScoped<IServiceRepository>(_ => new YamlServiceRepository(collection));
+        builder.Services.AddScoped<IResourceRepository>(_ => new YamlResourceRepository(collection));
 
 
         builder.Services.AddUseCases();

+ 0 - 7
RackPeek.Web/RackPeek.Web.csproj

@@ -36,13 +36,6 @@
 
     <ItemGroup>
         <AdditionalFiles Include="Components\Components\HardwareDependencyTreeComponent.razor"/>
-        <AdditionalFiles Include="Components\Components\ServerCardComponent.razor"/>
-        <AdditionalFiles Include="Components\Components\ServersListComponent.razor"/>
-        <AdditionalFiles Include="Components\Components\ServiceCardComponent.razor"/>
-        <AdditionalFiles Include="Components\Components\ServicesListComponent.razor"/>
-        <AdditionalFiles Include="Components\Components\SystemCardComponent.razor"/>
-        <AdditionalFiles Include="Components\Components\SystemDependencyTreeComponent.razor"/>
-        <AdditionalFiles Include="Components\Components\SystemsListComponent.razor"/>
     </ItemGroup>
 
 </Project>

+ 1 - 8
RackPeek/Commands/Desktops/Cpus/DesktopCpuAddCommand.cs

@@ -17,14 +17,7 @@ public class DesktopCpuAddCommand(IServiceProvider provider)
         using var scope = provider.CreateScope();
         var useCase = scope.ServiceProvider.GetRequiredService<AddDesktopCpuUseCase>();
 
-        var cpu = new Cpu
-        {
-            Model = settings.Model,
-            Cores = settings.Cores,
-            Threads = settings.Threads
-        };
-
-        await useCase.ExecuteAsync(settings.DesktopName, cpu);
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Model, settings.Cores, settings.Threads);
 
         AnsiConsole.MarkupLine($"[green]CPU added to desktop '{settings.DesktopName}'.[/]");
         return 0;

+ 2 - 0
RackPeek/Commands/Servers/ServerSetCommand.cs

@@ -8,6 +8,7 @@ namespace RackPeek.Commands.Servers;
 public class ServerSetSettings : ServerNameSettings
 {
     [CommandOption("--ram <GB>")] public int RamGb { get; set; }
+    [CommandOption("--ram_mts <MTs>")] public int RamMts { get; set; }
 
     [CommandOption("--ipmi")] public bool Ipmi { get; set; }
 }
@@ -27,6 +28,7 @@ public class ServerSetCommand(
         await useCase.ExecuteAsync(
             settings.Name,
             settings.RamGb,
+            settings.RamMts,
             settings.Ipmi);
 
         AnsiConsole.MarkupLine($"[green]Server '{settings.Name}' updated.[/]");

+ 168 - 65
RackPeek/Yaml/YamlResourceCollection.cs

@@ -13,27 +13,74 @@ public sealed class YamlResourceCollection(bool watch) : IDisposable
 {
     private static readonly TimeSpan ReloadDebounce = TimeSpan.FromMilliseconds(300);
 
+    private readonly object _sync = new();
+
     private readonly List<ResourceEntry> _entries = [];
     private readonly List<string> _knownFiles = [];
     private readonly ConcurrentDictionary<string, DateTime> _reloadQueue = [];
     private readonly bool _watch = watch;
     private readonly Dictionary<string, FileSystemWatcher> _watchers = [];
 
-    public IReadOnlyList<string> SourceFiles => _knownFiles.ToList();
+    public IReadOnlyList<string> SourceFiles
+    {
+        get
+        {
+            lock (_sync)
+                return _knownFiles.ToList();
+        }
+    }
 
-    public IReadOnlyList<Hardware> HardwareResources =>
-        _entries.Select(e => e.Resource).OfType<Hardware>().ToList();
+    public IReadOnlyList<Hardware> HardwareResources
+    {
+        get
+        {
+            lock (_sync)
+            {
+                return _entries
+                    .Select(e => e.Resource)
+                    .OfType<Hardware>()
+                    .ToList();
+            }
+        }
+    }
 
-    public IReadOnlyList<SystemResource> SystemResources =>
-        _entries.Select(e => e.Resource).OfType<SystemResource>().ToList();
+    public IReadOnlyList<SystemResource> SystemResources
+    {
+        get
+        {
+            lock (_sync)
+            {
+                return _entries
+                    .Select(e => e.Resource)
+                    .OfType<SystemResource>()
+                    .ToList();
+            }
+        }
+    }
 
-    public IReadOnlyList<Service> ServiceResources =>
-        _entries.Select(e => e.Resource).OfType<Service>().ToList();
+    public IReadOnlyList<Service> ServiceResources
+    {
+        get
+        {
+            lock (_sync)
+            {
+                return _entries
+                    .Select(e => e.Resource)
+                    .OfType<Service>()
+                    .ToList();
+            }
+        }
+    }
 
     public void Dispose()
     {
-        foreach (var watcher in _watchers.Values)
-            watcher.Dispose();
+        lock (_sync)
+        {
+            foreach (var watcher in _watchers.Values)
+                watcher.Dispose();
+
+            _watchers.Clear();
+        }
     }
 
     // ----------------------------
@@ -45,7 +92,6 @@ public sealed class YamlResourceCollection(bool watch) : IDisposable
         foreach (var file in filePaths)
         {
             TrackFile(file);
-
             LoadFile(file);
         }
     }
@@ -53,20 +99,35 @@ public sealed class YamlResourceCollection(bool watch) : IDisposable
     public void Load(string yaml, string file)
     {
         TrackFile(file);
-        foreach (var resource in Deserialize(yaml))
-            _entries.Add(new ResourceEntry(resource, file));
+
+        var newEntries = Deserialize(yaml)
+            .Where(r => r != null)
+            .Select(r => new ResourceEntry(r!, file))
+            .ToList();
+
+        lock (_sync)
+        {
+            RemoveEntriesFromFile(file);
+            _entries.AddRange(newEntries);
+        }
     }
 
     private void LoadFile(string file)
     {
-        RemoveEntriesFromFile(file);
-
         var yaml = File.Exists(file)
             ? SafeReadAllText(file)
             : string.Empty;
 
-        foreach (var resource in Deserialize(yaml))
-            _entries.Add(new ResourceEntry(resource, file));
+        var newEntries = Deserialize(yaml)
+            .Where(r => r != null)
+            .Select(r => new ResourceEntry(r!, file))
+            .ToList();
+
+        lock (_sync)
+        {
+            RemoveEntriesFromFile(file);
+            _entries.AddRange(newEntries);
+        }
     }
 
     // ----------------------------
@@ -75,48 +136,59 @@ public sealed class YamlResourceCollection(bool watch) : IDisposable
 
     private void TrackFile(string file)
     {
-        if (!_knownFiles.Contains(file))
-            _knownFiles.Add(file);
-
-        var directory = Path.GetDirectoryName(file)!;
-
-        if (_watchers.ContainsKey(directory) || !_watch)
-            return;
-
-        var watcher = new FileSystemWatcher(directory)
+        lock (_sync)
         {
-            EnableRaisingEvents = true,
-            NotifyFilter = NotifyFilters.LastWrite
-                           | NotifyFilters.FileName
-                           | NotifyFilters.Size
-        };
+            if (!_knownFiles.Contains(file))
+                _knownFiles.Add(file);
 
-        watcher.Changed += OnFileChanged;
-        watcher.Created += OnFileChanged;
-        watcher.Deleted += OnFileChanged;
-        watcher.Renamed += OnFileRenamed;
+            var directory = Path.GetDirectoryName(file);
+            if (directory == null || !_watch || _watchers.ContainsKey(directory))
+                return;
 
-        _watchers[directory] = watcher;
+            var watcher = new FileSystemWatcher(directory)
+            {
+                EnableRaisingEvents = true,
+                NotifyFilter = NotifyFilters.LastWrite
+                               | NotifyFilters.FileName
+                               | NotifyFilters.Size
+            };
+
+            watcher.Changed += OnFileChanged;
+            watcher.Created += OnFileChanged;
+            watcher.Deleted += OnFileChanged;
+            watcher.Renamed += OnFileRenamed;
+
+            _watchers[directory] = watcher;
+        }
     }
 
     private void OnFileChanged(object sender, FileSystemEventArgs e)
     {
-        if (!_knownFiles.Contains(e.FullPath))
-            return;
+        lock (_sync)
+        {
+            if (!_knownFiles.Contains(e.FullPath))
+                return;
+        }
 
         QueueReload(e.FullPath);
     }
 
     private void OnFileRenamed(object sender, RenamedEventArgs e)
     {
-        if (_knownFiles.Contains(e.OldFullPath))
+        lock (_sync)
         {
-            RemoveEntriesFromFile(e.OldFullPath);
-            _knownFiles.Remove(e.OldFullPath);
+            if (_knownFiles.Contains(e.OldFullPath))
+            {
+                RemoveEntriesFromFile(e.OldFullPath);
+                _knownFiles.Remove(e.OldFullPath);
+            }
         }
 
-        if (_knownFiles.Contains(e.FullPath))
-            QueueReload(e.FullPath);
+        lock (_sync)
+        {
+            if (_knownFiles.Contains(e.FullPath))
+                QueueReload(e.FullPath);
+        }
     }
 
     private void QueueReload(string file)
@@ -141,37 +213,51 @@ public sealed class YamlResourceCollection(bool watch) : IDisposable
     public void Add(Resource resource, string sourceFile)
     {
         TrackFile(sourceFile);
-        _entries.Add(new ResourceEntry(resource, sourceFile));
+
+        lock (_sync)
+        {
+            _entries.Add(new ResourceEntry(resource, sourceFile));
+        }
     }
 
     public void Update(Resource resource)
     {
-        var existing = _entries.FirstOrDefault(e =>
-            e.Resource.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase));
+        lock (_sync)
+        {
+            var existing = _entries.FirstOrDefault(e =>
+                e.Resource.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase));
 
-        if (existing == null)
-            throw new InvalidOperationException($"Resource '{resource.Name}' not found.");
+            if (existing == null)
+                throw new InvalidOperationException($"Resource '{resource.Name}' not found.");
 
-        _entries.Remove(existing);
-        _entries.Add(new ResourceEntry(resource, existing.SourceFile));
+            _entries.Remove(existing);
+            _entries.Add(new ResourceEntry(resource, existing.SourceFile));
+        }
     }
 
     public void Delete(string name)
     {
-        var existing = _entries.FirstOrDefault(e =>
-            e.Resource.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
+        lock (_sync)
+        {
+            var existing = _entries.FirstOrDefault(e =>
+                e.Resource.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
 
-        if (existing == null)
-            throw new InvalidOperationException($"Resource '{name}' not found.");
+            if (existing == null)
+                throw new InvalidOperationException($"Resource '{name}' not found.");
 
-        _entries.Remove(existing);
+            _entries.Remove(existing);
+        }
     }
 
     public Resource? GetByName(string name)
     {
-        return _entries
-            .Select(e => e.Resource)
-            .FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
+        lock (_sync)
+        {
+            return _entries
+                .Select(e => e.Resource)
+                .FirstOrDefault(r =>
+                    r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
+        }
     }
 
     private void RemoveEntriesFromFile(string file)
@@ -185,9 +271,18 @@ public sealed class YamlResourceCollection(bool watch) : IDisposable
 
     public void SaveAll()
     {
-        foreach (var file in _knownFiles)
+        List<string> files;
+        List<ResourceEntry> snapshot;
+
+        lock (_sync)
+        {
+            files = _knownFiles.ToList();
+            snapshot = _entries.ToList();
+        }
+
+        foreach (var file in files)
         {
-            var resources = _entries
+            var resources = snapshot
                 .Where(e => e.SourceFile == file)
                 .Select(e => e.Resource);
 
@@ -237,7 +332,7 @@ public sealed class YamlResourceCollection(bool watch) : IDisposable
 
         var props = new DeserializerBuilder()
             .Build()
-            .Deserialize<Dictionary<string, object?>>(yaml);
+            .Deserialize<Dictionary<string, object?>>(yaml) ?? new();
 
         foreach (var (key, value) in props)
             if (key != "kind")
@@ -267,13 +362,16 @@ public sealed class YamlResourceCollection(bool watch) : IDisposable
 
         foreach (var item in items)
         {
-            var kind = item["kind"].ToString();
+            if (!item.TryGetValue("kind", out var kindObj) || kindObj == null)
+                continue;
+
+            var kind = kindObj.ToString();
             var typedYaml = new SerializerBuilder()
                 .WithNamingConvention(CamelCaseNamingConvention.Instance)
                 .Build()
                 .Serialize(item);
 
-            resources.Add(kind switch
+            Resource resource = kind switch
             {
                 "Server" => deserializer.Deserialize<Server>(typedYaml),
                 "Switch" => deserializer.Deserialize<Switch>(typedYaml),
@@ -285,8 +383,11 @@ public sealed class YamlResourceCollection(bool watch) : IDisposable
                 "Ups" => deserializer.Deserialize<Ups>(typedYaml),
                 "System" => deserializer.Deserialize<SystemResource>(typedYaml),
                 "Service" => deserializer.Deserialize<Service>(typedYaml),
-                _ => throw new InvalidOperationException($"Unknown kind: {kind}")
-            });
+                _ => null
+            };
+
+            if (resource != null)
+                resources.Add(resource);
         }
 
         return resources;
@@ -295,6 +396,7 @@ public sealed class YamlResourceCollection(bool watch) : IDisposable
     private static string SafeReadAllText(string file)
     {
         for (var i = 0; i < 5; i++)
+        {
             try
             {
                 return File.ReadAllText(file);
@@ -303,9 +405,10 @@ public sealed class YamlResourceCollection(bool watch) : IDisposable
             {
                 Thread.Sleep(50);
             }
+        }
 
         return string.Empty;
     }
 
     private sealed record ResourceEntry(Resource Resource, string SourceFile);
-}
+}

+ 1 - 0
Tests/EndToEnd/ServiceYamlE2ETests.cs

@@ -45,6 +45,7 @@ public class ServiceYamlE2ETests(TempYamlCliFixture fs, ITestOutputHelper output
 
         Assert.Equal("Service 'immich' updated.\n", output);
 
+        outputHelper.WriteLine(yaml);
         Assert.Equal("""
                      resources:
                      - kind: System

+ 7 - 4
Tests/HardwareResources/AddDriveUseCaseTests.cs

@@ -1,4 +1,5 @@
 using NSubstitute;
+using RackPeek.Domain.Helpers;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware.Models;
 using RackPeek.Domain.Resources.Hardware.Servers.Drives;
@@ -85,10 +86,12 @@ public class AddDrivesUseCaseTests
         var sut = new AddDrivesUseCase(repo);
 
         // Act
-        await sut.ExecuteAsync(
-            "node01",
-            "NVMe",
-            2000
+        var ex = await Assert.ThrowsAsync<NotFoundException>(async () =>
+            await sut.ExecuteAsync(
+                "node01",
+                "NVMe",
+                2000
+            )
         );
 
         // Assert

+ 5 - 2
Tests/HardwareResources/RemoveDriveUseCaseTests.cs

@@ -1,4 +1,5 @@
 using NSubstitute;
+using RackPeek.Domain.Helpers;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware.Models;
 using RackPeek.Domain.Resources.Hardware.Servers.Drives;
@@ -80,8 +81,10 @@ public class RemoveDriveUseCaseTests
         var sut = new RemoveDriveUseCase(repo);
 
         // Act
-        await sut.ExecuteAsync("node01", 0);
-
+        var ex = await Assert.ThrowsAsync<NotFoundException>(async () =>
+            await sut.ExecuteAsync("node01", 0)
+        );
+        
         // Assert
         await repo.DidNotReceive().UpdateAsync(Arg.Any<Server>());
     }

+ 9 - 6
Tests/HardwareResources/UpdateDriveUseCaseTests.cs

@@ -1,4 +1,5 @@
 using NSubstitute;
+using RackPeek.Domain.Helpers;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware.Models;
 using RackPeek.Domain.Resources.Hardware.Servers.Drives;
@@ -88,13 +89,15 @@ public class UpdateDriveUseCaseTests
         var sut = new UpdateDriveUseCase(repo);
 
         // Act
-        await sut.ExecuteAsync(
-            "node01",
-            0,
-            "SATA",
-            500
+        var ex = await Assert.ThrowsAsync<NotFoundException>(async () =>
+            await sut.ExecuteAsync(
+                "node01",
+                0,
+                "SATA",
+                500
+            )
         );
-
+        
         // Assert
         await repo.DidNotReceive().UpdateAsync(Arg.Any<Server>());
     }

+ 2 - 0
Tests/HardwareResources/UpdateServerUseCaseTests.cs

@@ -30,6 +30,7 @@ public class UpdateServerUseCaseTests
         await sut.ExecuteAsync(
             "node01",
             64,
+            3200,
             true
         );
 
@@ -37,6 +38,7 @@ public class UpdateServerUseCaseTests
         await repo.Received(1).UpdateAsync(Arg.Is<Server>(s =>
             s.Name == "node01" &&
             s.Ram.Size == 64 &&
+            s.Ram.Mts == 3200 &&
             s.Ipmi == true
         ));
     }

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff