Просмотр исходного кода

Merge pull request #25 from Timmoth/UPS-Desktop-Access-Points

UPS, Desktop, Access Points Top Level Commands
Chester-alt 2 месяцев назад
Родитель
Сommit
04571bf7ea
95 измененных файлов с 2756 добавлено и 1 удалено
  1. 20 0
      RackPeek.Domain/Resources/Hardware/AccessPoints/AddAccessPointUseCase.cs
  2. 14 0
      RackPeek.Domain/Resources/Hardware/AccessPoints/DeleteAccessPointUseCase.cs
  3. 25 0
      RackPeek.Domain/Resources/Hardware/AccessPoints/DescribeAccessPointUseCase.cs
  4. 12 0
      RackPeek.Domain/Resources/Hardware/AccessPoints/GetAccessPointUseCase.cs
  5. 12 0
      RackPeek.Domain/Resources/Hardware/AccessPoints/GetAccessPointsUseCase.cs
  6. 25 0
      RackPeek.Domain/Resources/Hardware/AccessPoints/UpdateAccessPointUseCase.cs
  7. 32 0
      RackPeek.Domain/Resources/Hardware/Desktop/AddDesktopUseCase.cs
  8. 17 0
      RackPeek.Domain/Resources/Hardware/Desktop/Cpu/AddDesktopCpuUseCase.cs
  9. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Cpu/RemoveDesktopCpuUseCase.cs
  10. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Cpu/UpdateDesktopCpuUseCase.cs
  11. 22 0
      RackPeek.Domain/Resources/Hardware/Desktop/DeleteDesktopUseCase.cs
  12. 37 0
      RackPeek.Domain/Resources/Hardware/Desktop/DescribeDesktopUseCase.cs
  13. 17 0
      RackPeek.Domain/Resources/Hardware/Desktop/Drive/AddDesktopDriveUseCase.cs
  14. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Drive/RemoveDesktopDriveUseCase.cs
  15. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Drive/UpdateDesktopDriveUseCase.cs
  16. 12 0
      RackPeek.Domain/Resources/Hardware/Desktop/GetDesktopUseCase.cs
  17. 12 0
      RackPeek.Domain/Resources/Hardware/Desktop/GetDesktopsUseCase.cs
  18. 17 0
      RackPeek.Domain/Resources/Hardware/Desktop/Gpu/AddDesktopGpuUseCase.cs
  19. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Gpu/RemoveDesktopGpuUseCase.cs
  20. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Gpu/UpdateDesktopGpuUseCase.cs
  21. 17 0
      RackPeek.Domain/Resources/Hardware/Desktop/Nic/AddDesktopNicUseCase.cs
  22. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Nic/RemoveDesktopNicUseCase.cs
  23. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Nic/UpdateDesktopNicUseCase.cs
  24. 21 0
      RackPeek.Domain/Resources/Hardware/Desktop/UpdateDesktopUseCase.cs
  25. 2 0
      RackPeek.Domain/Resources/Hardware/Models/Desktop.cs
  26. 1 1
      RackPeek.Domain/Resources/Hardware/Reports/DesktopHardwareReport.cs
  27. 20 0
      RackPeek.Domain/Resources/Hardware/UpsUnits/AddUpsUseCase.cs
  28. 14 0
      RackPeek.Domain/Resources/Hardware/UpsUnits/DeleteUpsUseCase.cs
  29. 25 0
      RackPeek.Domain/Resources/Hardware/UpsUnits/DescribeUpsUseCase.cs
  30. 12 0
      RackPeek.Domain/Resources/Hardware/UpsUnits/GetUpsUnitUseCase.cs
  31. 12 0
      RackPeek.Domain/Resources/Hardware/UpsUnits/GetUpsUseCase.cs
  32. 25 0
      RackPeek.Domain/Resources/Hardware/UpsUnits/UpdateUpsUseCase.cs
  33. 9 0
      RackPeek/Commands/AccessPoints/AccessCommands.cs
  34. 31 0
      RackPeek/Commands/AccessPoints/AccessPointAddCommand.cs
  35. 25 0
      RackPeek/Commands/AccessPoints/AccessPointDeleteCommand.cs
  36. 43 0
      RackPeek/Commands/AccessPoints/AccessPointDescribeCommand.cs
  37. 33 0
      RackPeek/Commands/AccessPoints/AccessPointGetByNameCommand.cs
  38. 44 0
      RackPeek/Commands/AccessPoints/AccessPointGetCommand.cs
  39. 39 0
      RackPeek/Commands/AccessPoints/AccessPointSetCommand.cs
  40. 32 0
      RackPeek/Commands/Desktop/Cpu/DesktopCpuAddCommand.cs
  41. 18 0
      RackPeek/Commands/Desktop/Cpu/DesktopCpuAddSettings.cs
  42. 24 0
      RackPeek/Commands/Desktop/Cpu/DesktopCpuRemoveCommand.cs
  43. 12 0
      RackPeek/Commands/Desktop/Cpu/DesktopCpuRemoveSettings.cs
  44. 32 0
      RackPeek/Commands/Desktop/Cpu/DesktopCpuSetCommand.cs
  45. 21 0
      RackPeek/Commands/Desktop/Cpu/DesktopCpuSetSettings.cs
  46. 24 0
      RackPeek/Commands/Desktop/DesktopAddCommand.cs
  47. 9 0
      RackPeek/Commands/Desktop/DesktopCommands.cs
  48. 24 0
      RackPeek/Commands/Desktop/DesktopDeleteCommand.cs
  49. 41 0
      RackPeek/Commands/Desktop/DesktopDescribeCommand.cs
  50. 30 0
      RackPeek/Commands/Desktop/DesktopGetByNameCommand.cs
  51. 52 0
      RackPeek/Commands/Desktop/DesktopGetCommand.cs
  52. 30 0
      RackPeek/Commands/Desktop/DesktopSetCommand.cs
  53. 31 0
      RackPeek/Commands/Desktop/Drive/DesktopDriveAddCommand.cs
  54. 15 0
      RackPeek/Commands/Desktop/Drive/DesktopDriveAddSettings.cs
  55. 24 0
      RackPeek/Commands/Desktop/Drive/DesktopDriveRemoveCommand.cs
  56. 12 0
      RackPeek/Commands/Desktop/Drive/DesktopDriveRemoveSettings.cs
  57. 31 0
      RackPeek/Commands/Desktop/Drive/DesktopDriveSetCommand.cs
  58. 18 0
      RackPeek/Commands/Desktop/Drive/DesktopDriveSetSettings.cs
  59. 31 0
      RackPeek/Commands/Desktop/Gpu/DesktopGpuAddCommand.cs
  60. 15 0
      RackPeek/Commands/Desktop/Gpu/DesktopGpuAddSettings.cs
  61. 24 0
      RackPeek/Commands/Desktop/Gpu/DesktopGpuRemoveCommand.cs
  62. 12 0
      RackPeek/Commands/Desktop/Gpu/DesktopGpuRemoveSettings.cs
  63. 31 0
      RackPeek/Commands/Desktop/Gpu/DesktopGpuSetCommand.cs
  64. 18 0
      RackPeek/Commands/Desktop/Gpu/DesktopGpuSetSettings.cs
  65. 32 0
      RackPeek/Commands/Desktop/Nic/DesktopNicAddCommand.cs
  66. 18 0
      RackPeek/Commands/Desktop/Nic/DesktopNicAddSettings.cs
  67. 24 0
      RackPeek/Commands/Desktop/Nic/DesktopNicRemoveCommand.cs
  68. 12 0
      RackPeek/Commands/Desktop/Nic/DesktopNicRemoveSettings.cs
  69. 32 0
      RackPeek/Commands/Desktop/Nic/DesktopNicSetCommand.cs
  70. 21 0
      RackPeek/Commands/Desktop/Nic/DesktopNicSetSettings.cs
  71. 29 0
      RackPeek/Commands/Ups/UpsAddCommand.cs
  72. 29 0
      RackPeek/Commands/Ups/UpsDeleteCommand.cs
  73. 39 0
      RackPeek/Commands/Ups/UpsDescribeCommand.cs
  74. 32 0
      RackPeek/Commands/Ups/UpsGetByNameCommand.cs
  75. 42 0
      RackPeek/Commands/Ups/UpsGetCommand.cs
  76. 31 0
      RackPeek/Commands/Ups/UpsSetCommand.cs
  77. 200 0
      RackPeek/Program.cs
  78. 48 0
      Tests/Hardware/AccessPoints/AddAccessPointUseCaseTests.cs
  79. 45 0
      Tests/Hardware/AccessPoints/DeleteAccessPointUseCaseTests.cs
  80. 50 0
      Tests/Hardware/AccessPoints/DescribeAccessPointUseCaseTests.cs
  81. 43 0
      Tests/Hardware/AccessPoints/GetAccessPointUseCaseTests.cs
  82. 51 0
      Tests/Hardware/AccessPoints/GetAccessPointsUseCaseTest.cs
  83. 87 0
      Tests/Hardware/AccessPoints/UpdateAccessPointUseCaseTests.cs
  84. 31 0
      Tests/Hardware/Desktop/AddDesktopUseCaseTests.cs
  85. 31 0
      Tests/Hardware/Desktop/DeleteDesktopUseCaseTests.cs
  86. 47 0
      Tests/Hardware/Desktop/DescribeDesktopUseCaseTests.cs
  87. 34 0
      Tests/Hardware/Desktop/GetDesktopUseCaseTests.cs
  88. 24 0
      Tests/Hardware/Desktop/GetDesktopsUseCaseTests.cs
  89. 34 0
      Tests/Hardware/Desktop/UpdateDesktopUseCaseTests.cs
  90. 40 0
      Tests/Hardware/Ups/AddUpsUseCaseTests.cs
  91. 38 0
      Tests/Hardware/Ups/DeleteUpsUseCaseTests.cs
  92. 43 0
      Tests/Hardware/Ups/DescribeUpsUseCaseTests.cs
  93. 37 0
      Tests/Hardware/Ups/GetUpsUnitUseCaseTests.cs
  94. 44 0
      Tests/Hardware/Ups/GetUpsUseCaseTests.cs
  95. 78 0
      Tests/Hardware/Ups/UpdateUpsUseCaseTests.cs

+ 20 - 0
RackPeek.Domain/Resources/Hardware/AccessPoints/AddAccessPointUseCase.cs

@@ -0,0 +1,20 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.AccessPoints;
+
+public class AddAccessPointUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string name)
+    {
+        var existing = await repository.GetByNameAsync(name);
+        if (existing != null)
+            throw new InvalidOperationException($"Access point '{name}' already exists.");
+
+        var ap = new AccessPoint
+        {
+            Name = name
+        };
+
+        await repository.AddAsync(ap);
+    }
+}

+ 14 - 0
RackPeek.Domain/Resources/Hardware/AccessPoints/DeleteAccessPointUseCase.cs

@@ -0,0 +1,14 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.AccessPoints;
+
+public class DeleteAccessPointUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string name)
+    {
+        if (await repository.GetByNameAsync(name) is not AccessPoint ap)
+            throw new InvalidOperationException($"Access point '{name}' not found.");
+
+        await repository.DeleteAsync(name);
+    }
+}

+ 25 - 0
RackPeek.Domain/Resources/Hardware/AccessPoints/DescribeAccessPointUseCase.cs

@@ -0,0 +1,25 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.AccessPoints;
+
+public record AccessPointDescription(
+    string Name,
+    string? Model,
+    double? Speed
+);
+
+public class DescribeAccessPointUseCase(IHardwareRepository repository)
+{
+    public async Task<AccessPointDescription?> ExecuteAsync(string name)
+    {
+        var ap = await repository.GetByNameAsync(name) as AccessPoint;
+        if (ap == null)
+            return null;
+
+        return new AccessPointDescription(
+            ap.Name,
+            ap.Model,
+            ap.Speed
+        );
+    }
+}

+ 12 - 0
RackPeek.Domain/Resources/Hardware/AccessPoints/GetAccessPointUseCase.cs

@@ -0,0 +1,12 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.AccessPoints;
+
+public class GetAccessPointUseCase(IHardwareRepository repository)
+{
+    public async Task<AccessPoint?> ExecuteAsync(string name)
+    {
+        var hardware = await repository.GetByNameAsync(name);
+        return hardware as AccessPoint;
+    }
+}

+ 12 - 0
RackPeek.Domain/Resources/Hardware/AccessPoints/GetAccessPointsUseCase.cs

@@ -0,0 +1,12 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.AccessPoints;
+
+public class GetAccessPointsUseCase(IHardwareRepository repository)
+{
+    public async Task<IReadOnlyList<AccessPoint>> ExecuteAsync()
+    {
+        var hardware = await repository.GetAllAsync();
+        return hardware.OfType<AccessPoint>().ToList();
+    }
+}

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

@@ -0,0 +1,25 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.AccessPoints;
+
+public class UpdateAccessPointUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string name,
+        string? model = null,
+        double? speed = null
+    )
+    {
+        var ap = await repository.GetByNameAsync(name) as AccessPoint;
+        if (ap == null)
+            throw new InvalidOperationException($"Access point '{name}' not found.");
+
+        if (!string.IsNullOrWhiteSpace(model))
+            ap.Model = model;
+
+        if (speed.HasValue)
+            ap.Speed = speed.Value;
+
+        await repository.UpdateAsync(ap);
+    }
+}

+ 32 - 0
RackPeek.Domain/Resources/Hardware/Desktop/AddDesktopUseCase.cs

@@ -0,0 +1,32 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class AddDesktopUseCase
+{
+    private readonly IHardwareRepository _repository;
+
+    public AddDesktopUseCase(IHardwareRepository repository)
+    {
+        _repository = repository;
+    }
+
+    public async Task ExecuteAsync(string name)
+    {
+        var existing = await _repository.GetByNameAsync(name);
+        if (existing != null)
+            throw new InvalidOperationException($"Desktop '{name}' already exists.");
+
+        var desktop = new Models.Desktop
+        {
+            Name = name,
+            Cpus = new List<Cpu>(),
+            Drives = new List<Drive>(),
+            Nics = new List<Nic>(),
+            Gpus = new List<Gpu>(),
+            Ram = null
+        };
+
+        await _repository.AddAsync(desktop);
+    }
+}

+ 17 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Cpu/AddDesktopCpuUseCase.cs

@@ -0,0 +1,17 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class AddDesktopCpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, Cpu cpu)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        desktop.Cpus ??= new List<Cpu>();
+        desktop.Cpus.Add(cpu);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Cpu/RemoveDesktopCpuUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class RemoveDesktopCpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Cpus == null || index < 0 || index >= desktop.Cpus.Count)
+            throw new InvalidOperationException($"CPU index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Cpus.RemoveAt(index);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Cpu/UpdateDesktopCpuUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class UpdateDesktopCpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index, Cpu updated)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Cpus == null || index < 0 || index >= desktop.Cpus.Count)
+            throw new InvalidOperationException($"CPU index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Cpus[index] = updated;
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 22 - 0
RackPeek.Domain/Resources/Hardware/Desktop/DeleteDesktopUseCase.cs

@@ -0,0 +1,22 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class DeleteDesktopUseCase
+{
+    private readonly IHardwareRepository _repository;
+
+    public DeleteDesktopUseCase(IHardwareRepository repository)
+    {
+        _repository = repository;
+    }
+
+    public async Task ExecuteAsync(string name)
+    {
+        var hardware = await _repository.GetByNameAsync(name);
+        if (hardware == null)
+            throw new InvalidOperationException($"Desktop '{name}' not found.");
+
+        await _repository.DeleteAsync(name);
+    }
+}

+ 37 - 0
RackPeek.Domain/Resources/Hardware/Desktop/DescribeDesktopUseCase.cs

@@ -0,0 +1,37 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public record DesktopDescription(
+    string Name,
+    string? Model,
+    int CpuCount,
+    string? RamSummary,
+    int DriveCount,
+    int NicCount,
+    int GpuCount
+);
+
+public class DescribeDesktopUseCase(IHardwareRepository repository)
+{
+    public async Task<DesktopDescription?> ExecuteAsync(string name)
+    {
+        var desktop = await repository.GetByNameAsync(name) as Models.Desktop;
+        if (desktop == null)
+            return null;
+
+        var ramSummary = desktop.Ram == null
+            ? "None"
+            : $"{desktop.Ram.Size} GB @ {desktop.Ram.Mts} MT/s";
+
+        return new DesktopDescription(
+            desktop.Name,
+            desktop.Model,
+            desktop.Cpus?.Count ?? 0,
+            ramSummary,
+            desktop.Drives?.Count ?? 0,
+            desktop.Nics?.Count ?? 0,
+            desktop.Gpus?.Count ?? 0
+        );
+    }
+}

+ 17 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Drive/AddDesktopDriveUseCase.cs

@@ -0,0 +1,17 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class AddDesktopDriveUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, Drive drive)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        desktop.Drives ??= new List<Drive>();
+        desktop.Drives.Add(drive);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Drive/RemoveDesktopDriveUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class RemoveDesktopDriveUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Drives == null || index < 0 || index >= desktop.Drives.Count)
+            throw new InvalidOperationException($"Drive index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Drives.RemoveAt(index);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Drive/UpdateDesktopDriveUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class UpdateDesktopDriveUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index, Drive updated)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Drives == null || index < 0 || index >= desktop.Drives.Count)
+            throw new InvalidOperationException($"Drive index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Drives[index] = updated;
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 12 - 0
RackPeek.Domain/Resources/Hardware/Desktop/GetDesktopUseCase.cs

@@ -0,0 +1,12 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class GetDesktopUseCase(IHardwareRepository repository)
+{
+    public async Task<Models.Desktop?> ExecuteAsync(string name)
+    {
+        var hardware = await repository.GetByNameAsync(name);
+        return hardware as Models.Desktop;
+    }
+}

+ 12 - 0
RackPeek.Domain/Resources/Hardware/Desktop/GetDesktopsUseCase.cs

@@ -0,0 +1,12 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class GetDesktopsUseCase(IHardwareRepository repository)
+{
+    public async Task<IReadOnlyList<Models.Desktop>> ExecuteAsync()
+    {
+        var hardware = await repository.GetAllAsync();
+        return hardware.OfType<Models.Desktop>().ToList();
+    }
+}

+ 17 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Gpu/AddDesktopGpuUseCase.cs

@@ -0,0 +1,17 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class AddDesktopGpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, Gpu gpu)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        desktop.Gpus ??= new List<Gpu>();
+        desktop.Gpus.Add(gpu);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Gpu/RemoveDesktopGpuUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class RemoveDesktopGpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Gpus == null || index < 0 || index >= desktop.Gpus.Count)
+            throw new InvalidOperationException($"GPU index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Gpus.RemoveAt(index);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Gpu/UpdateDesktopGpuUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class UpdateDesktopGpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index, Gpu updated)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Gpus == null || index < 0 || index >= desktop.Gpus.Count)
+            throw new InvalidOperationException($"GPU index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Gpus[index] = updated;
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 17 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Nic/AddDesktopNicUseCase.cs

@@ -0,0 +1,17 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class AddDesktopNicUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, Nic nic)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        desktop.Nics ??= new List<Nic>();
+        desktop.Nics.Add(nic);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Nic/RemoveDesktopNicUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class RemoveDesktopNicUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Nics == null || index < 0 || index >= desktop.Nics.Count)
+            throw new InvalidOperationException($"NIC index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Nics.RemoveAt(index);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Nic/UpdateDesktopNicUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class UpdateDesktopNicUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index, Nic updated)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Nics == null || index < 0 || index >= desktop.Nics.Count)
+            throw new InvalidOperationException($"NIC index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Nics[index] = updated;
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 21 - 0
RackPeek.Domain/Resources/Hardware/Desktop/UpdateDesktopUseCase.cs

@@ -0,0 +1,21 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class UpdateDesktopUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string name,
+        string? model = null
+    )
+    {
+        var desktop = await repository.GetByNameAsync(name) as Models.Desktop;
+        if (desktop == null)
+            throw new InvalidOperationException($"Desktop '{name}' not found.");
+
+        if (!string.IsNullOrWhiteSpace(model))
+            desktop.Model = model;
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 2 - 0
RackPeek.Domain/Resources/Hardware/Models/Desktop.cs

@@ -7,4 +7,6 @@ public class Desktop : Hardware
     public List<Drive>? Drives { get; set; }
     public List<Nic>? Nics { get; set; }
     public List<Gpu>? Gpus { get; set; }
+    public string Model { get; set; }
+
 }

+ 1 - 1
RackPeek.Domain/Resources/Hardware/Reports/DesktopHardwareReport.cs

@@ -24,7 +24,7 @@ public class DesktopHardwareReportUseCase(IHardwareRepository repository)
     public async Task<DesktopHardwareReport> ExecuteAsync()
     {
         var hardware = await repository.GetAllAsync();
-        var desktops = hardware.OfType<Desktop>();
+        var desktops = hardware.OfType<Models.Desktop>();
 
         var rows = desktops.Select(desktop =>
         {

+ 20 - 0
RackPeek.Domain/Resources/Hardware/UpsUnits/AddUpsUseCase.cs

@@ -0,0 +1,20 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+public class AddUpsUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string name)
+    {
+        var existing = await repository.GetByNameAsync(name);
+        if (existing != null)
+            throw new InvalidOperationException($"UPS '{name}' already exists.");
+
+        var ups = new Ups
+        {
+            Name = name
+        };
+
+        await repository.AddAsync(ups);
+    }
+}

+ 14 - 0
RackPeek.Domain/Resources/Hardware/UpsUnits/DeleteUpsUseCase.cs

@@ -0,0 +1,14 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+public class DeleteUpsUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string name)
+    {
+        if (await repository.GetByNameAsync(name) is not Ups ups)
+            throw new InvalidOperationException($"UPS '{name}' not found.");
+
+        await repository.DeleteAsync(name);
+    }
+}

+ 25 - 0
RackPeek.Domain/Resources/Hardware/UpsUnits/DescribeUpsUseCase.cs

@@ -0,0 +1,25 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+public record UpsDescription(
+    string Name,
+    string? Model,
+    int? Va
+);
+
+public class DescribeUpsUseCase(IHardwareRepository repository)
+{
+    public async Task<UpsDescription?> ExecuteAsync(string name)
+    {
+        var ups = await repository.GetByNameAsync(name) as Ups;
+        if (ups == null)
+            return null;
+
+        return new UpsDescription(
+            ups.Name,
+            ups.Model,
+            ups.Va
+        );
+    }
+}

+ 12 - 0
RackPeek.Domain/Resources/Hardware/UpsUnits/GetUpsUnitUseCase.cs

@@ -0,0 +1,12 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+public class GetUpsUnitUseCase(IHardwareRepository repository)
+{
+    public async Task<Ups?> ExecuteAsync(string name)
+    {
+        var hardware = await repository.GetByNameAsync(name);
+        return hardware as Ups;
+    }
+}

+ 12 - 0
RackPeek.Domain/Resources/Hardware/UpsUnits/GetUpsUseCase.cs

@@ -0,0 +1,12 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+public class GetUpsUseCase(IHardwareRepository repository)
+{
+    public async Task<IReadOnlyList<Ups>> ExecuteAsync()
+    {
+        var hardware = await repository.GetAllAsync();
+        return hardware.OfType<Ups>().ToList();
+    }
+}

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

@@ -0,0 +1,25 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+public class UpdateUpsUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string name,
+        string? model = null,
+        int? va = null
+    )
+    {
+        var ups = await repository.GetByNameAsync(name) as Ups;
+        if (ups == null)
+            throw new InvalidOperationException($"UPS '{name}' not found.");
+
+        if (!string.IsNullOrWhiteSpace(model))
+            ups.Model = model;
+
+        if (va.HasValue)
+            ups.Va = va.Value;
+
+        await repository.UpdateAsync(ups);
+    }
+}

+ 9 - 0
RackPeek/Commands/AccessPoints/AccessCommands.cs

@@ -0,0 +1,9 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.AccessPoints;
+
+public class AccessPointNameSettings : CommandSettings
+{
+    [CommandArgument(0, "<name>")] 
+    public string Name { get; set; } = default!;
+}

+ 31 - 0
RackPeek/Commands/AccessPoints/AccessPointAddCommand.cs

@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.AccessPoints;
+
+public class AccessPointAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<name>")] 
+    public string Name { get; set; } = default!;
+}
+
+public class AccessPointAddCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<AccessPointAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        AccessPointAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddAccessPointUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine($"[green]Access point '{settings.Name}' added.[/]");
+        return 0;
+    }
+}

+ 25 - 0
RackPeek/Commands/AccessPoints/AccessPointDeleteCommand.cs

@@ -0,0 +1,25 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.AccessPoints;
+
+public class AccessPointDeleteCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<AccessPointNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        AccessPointNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DeleteAccessPointUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine($"[green]Access point '{settings.Name}' deleted.[/]");
+        return 0;
+    }
+}

+ 43 - 0
RackPeek/Commands/AccessPoints/AccessPointDescribeCommand.cs

@@ -0,0 +1,43 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.AccessPoints;
+
+public class AccessPointDescribeCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<AccessPointNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        AccessPointNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeAccessPointUseCase>();
+
+        var ap = await useCase.ExecuteAsync(settings.Name);
+
+        if (ap == null)
+        {
+            AnsiConsole.MarkupLine($"[red]Access point '{settings.Name}' not found.[/]");
+            return 1;
+        }
+
+        var grid = new Grid()
+            .AddColumn(new GridColumn().NoWrap())
+            .AddColumn(new GridColumn().NoWrap());
+
+        grid.AddRow("Name:", ap.Name);
+        grid.AddRow("Model:", ap.Model ?? "Unknown");
+        grid.AddRow("Speed (Gbps):", ap.Speed?.ToString() ?? "Unknown");
+
+        AnsiConsole.Write(
+            new Panel(grid)
+                .Header("Access Point")
+                .Border(BoxBorder.Rounded));
+
+        return 0;
+    }
+}

+ 33 - 0
RackPeek/Commands/AccessPoints/AccessPointGetByNameCommand.cs

@@ -0,0 +1,33 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.AccessPoints;
+
+public class AccessPointGetByNameCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<AccessPointNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        AccessPointNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeAccessPointUseCase>();
+
+        var ap = await useCase.ExecuteAsync(settings.Name);
+
+        if (ap == null)
+        {
+            AnsiConsole.MarkupLine($"[red]Access point '{settings.Name}' not found.[/]");
+            return 1;
+        }
+
+        AnsiConsole.MarkupLine(
+            $"[green]{ap.Name}[/]  Model: {ap.Model ?? "Unknown"}, Speed: {(ap.Speed?.ToString() ?? "Unknown")} Gbps");
+
+        return 0;
+    }
+}

+ 44 - 0
RackPeek/Commands/AccessPoints/AccessPointGetCommand.cs

@@ -0,0 +1,44 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Reports;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.AccessPoints;
+
+public class AccessPointGetCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AccessPointHardwareReportUseCase>();
+
+        var report = await useCase.ExecuteAsync();
+
+        if (report.AccessPoints.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No access points found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("Model")
+            .AddColumn("Speed (Gbps)");
+
+        foreach (var ap in report.AccessPoints)
+            table.AddRow(
+                ap.Name,
+                ap.Model,
+                ap.SpeedGb.ToString()
+            );
+
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}

+ 39 - 0
RackPeek/Commands/AccessPoints/AccessPointSetCommand.cs

@@ -0,0 +1,39 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Commands.Server;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.AccessPoints;
+
+public class AccessPointSetSettings : ServerNameSettings
+{
+    [CommandOption("--model")] 
+    public string? Model { get; set; }
+
+    [CommandOption("--speed")] 
+    public double? Speed { get; set; }
+}
+
+public class AccessPointSetCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<AccessPointSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        AccessPointSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateAccessPointUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Model,
+            settings.Speed
+        );
+
+        AnsiConsole.MarkupLine($"[green]Access point '{settings.Name}' updated.[/]");
+        return 0;
+    }
+}

+ 32 - 0
RackPeek/Commands/Desktop/Cpu/DesktopCpuAddCommand.cs

@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Cpus;
+
+public class DesktopCpuAddCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopCpuAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopCpuAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        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);
+
+        AnsiConsole.MarkupLine($"[green]CPU added to desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 18 - 0
RackPeek/Commands/Desktop/Cpu/DesktopCpuAddSettings.cs

@@ -0,0 +1,18 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Cpus;
+
+public class DesktopCpuAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandOption("--model")]
+    public string? Model { get; set; }
+
+    [CommandOption("--cores")]
+    public int? Cores { get; set; }
+
+    [CommandOption("--threads")]
+    public int? Threads { get; set; }
+}

+ 24 - 0
RackPeek/Commands/Desktop/Cpu/DesktopCpuRemoveCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Cpus;
+
+public class DesktopCpuRemoveCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopCpuRemoveSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopCpuRemoveSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RemoveDesktopCpuUseCase>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index);
+
+        AnsiConsole.MarkupLine($"[green]CPU #{settings.Index} removed from desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 12 - 0
RackPeek/Commands/Desktop/Cpu/DesktopCpuRemoveSettings.cs

@@ -0,0 +1,12 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Cpus;
+
+public class DesktopCpuRemoveSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+}

+ 32 - 0
RackPeek/Commands/Desktop/Cpu/DesktopCpuSetCommand.cs

@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Cpus;
+
+public class DesktopCpuSetCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopCpuSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopCpuSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateDesktopCpuUseCase>();
+
+        var cpu = new Cpu
+        {
+            Model = settings.Model,
+            Cores = settings.Cores,
+            Threads = settings.Threads
+        };
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index, cpu);
+
+        AnsiConsole.MarkupLine($"[green]CPU #{settings.Index} updated on desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 21 - 0
RackPeek/Commands/Desktop/Cpu/DesktopCpuSetSettings.cs

@@ -0,0 +1,21 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Cpus;
+
+public class DesktopCpuSetSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+
+    [CommandOption("--model")]
+    public string? Model { get; set; }
+
+    [CommandOption("--cores")]
+    public int? Cores { get; set; }
+
+    [CommandOption("--threads")]
+    public int? Threads { get; set; }
+}

+ 24 - 0
RackPeek/Commands/Desktop/DesktopAddCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop;
+
+public class DesktopAddCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddDesktopUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine($"[green]Desktop '{settings.Name}' added.[/]");
+        return 0;
+    }
+}

+ 9 - 0
RackPeek/Commands/Desktop/DesktopCommands.cs

@@ -0,0 +1,9 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop;
+
+public class DesktopNameSettings : CommandSettings
+{
+    [CommandArgument(0, "<name>")]
+    public string Name { get; set; } = default!;
+}

+ 24 - 0
RackPeek/Commands/Desktop/DesktopDeleteCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop;
+
+public class DesktopDeleteCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DeleteDesktopUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine($"[green]Desktop '{settings.Name}' deleted.[/]");
+        return 0;
+    }
+}

+ 41 - 0
RackPeek/Commands/Desktop/DesktopDescribeCommand.cs

@@ -0,0 +1,41 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop;
+
+public class DesktopDescribeCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeDesktopUseCase>();
+
+        var result = await useCase.ExecuteAsync(settings.Name);
+
+        if (result == null)
+        {
+            AnsiConsole.MarkupLine($"[red]Desktop '{settings.Name}' not found.[/]");
+            return 1;
+        }
+
+        var grid = new Grid().AddColumn().AddColumn();
+
+        grid.AddRow("Name:", result.Name);
+        grid.AddRow("Model:", result.Model ?? "Unknown");
+        grid.AddRow("CPUs:", result.CpuCount.ToString());
+        grid.AddRow("RAM:", result.RamSummary ?? "None");
+        grid.AddRow("Drives:", result.DriveCount.ToString());
+        grid.AddRow("NICs:", result.NicCount.ToString());
+        grid.AddRow("GPUs:", result.GpuCount.ToString());
+
+        AnsiConsole.Write(new Panel(grid).Header("Desktop").Border(BoxBorder.Rounded));
+
+        return 0;
+    }
+}

+ 30 - 0
RackPeek/Commands/Desktop/DesktopGetByNameCommand.cs

@@ -0,0 +1,30 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop;
+
+public class DesktopGetByNameCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<GetDesktopUseCase>();
+
+        var desktop = await useCase.ExecuteAsync(settings.Name);
+
+        if (desktop == null)
+        {
+            AnsiConsole.MarkupLine($"[red]Desktop '{settings.Name}' not found.[/]");
+            return 1;
+        }
+
+        AnsiConsole.MarkupLine($"[green]{desktop.Name}[/] (Model: {desktop.Model ?? "Unknown"})");
+        return 0;
+    }
+}

+ 52 - 0
RackPeek/Commands/Desktop/DesktopGetCommand.cs

@@ -0,0 +1,52 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop;
+
+public class DesktopGetCommand(IServiceProvider provider)
+    : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<GetDesktopsUseCase>();
+
+        var desktops = await useCase.ExecuteAsync();
+
+        if (desktops.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No desktops found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("Model")
+            .AddColumn("CPUs")
+            .AddColumn("RAM")
+            .AddColumn("Drives")
+            .AddColumn("NICs")
+            .AddColumn("GPUs");
+
+        foreach (var d in desktops)
+        {
+            table.AddRow(
+                d.Name,
+                d.Model ?? "Unknown",
+                (d.Cpus?.Count ?? 0).ToString(),
+                d.Ram == null ? "None" : $"{d.Ram.Size}GB",
+                (d.Drives?.Count ?? 0).ToString(),
+                (d.Nics?.Count ?? 0).ToString(),
+                (d.Gpus?.Count ?? 0).ToString()
+            );
+        }
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}

+ 30 - 0
RackPeek/Commands/Desktop/DesktopSetCommand.cs

@@ -0,0 +1,30 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop;
+
+public class DesktopSetSettings : DesktopNameSettings
+{
+    [CommandOption("--model")]
+    public string? Model { get; set; }
+}
+
+public class DesktopSetCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateDesktopUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name, settings.Model);
+
+        AnsiConsole.MarkupLine($"[green]Desktop '{settings.Name}' updated.[/]");
+        return 0;
+    }
+}

+ 31 - 0
RackPeek/Commands/Desktop/Drive/DesktopDriveAddCommand.cs

@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Drives;
+
+public class DesktopDriveAddCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopDriveAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopDriveAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddDesktopDriveUseCase>();
+
+        var drive = new Drive
+        {
+            Type = settings.Type,
+            Size = settings.Size
+        };
+
+        await useCase.ExecuteAsync(settings.DesktopName, drive);
+
+        AnsiConsole.MarkupLine($"[green]Drive added to desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 15 - 0
RackPeek/Commands/Desktop/Drive/DesktopDriveAddSettings.cs

@@ -0,0 +1,15 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Drives;
+
+public class DesktopDriveAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandOption("--type")]
+    public string? Type { get; set; }
+
+    [CommandOption("--size")]
+    public int? Size { get; set; }
+}

+ 24 - 0
RackPeek/Commands/Desktop/Drive/DesktopDriveRemoveCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Drives;
+
+public class DesktopDriveRemoveCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopDriveRemoveSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopDriveRemoveSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RemoveDesktopDriveUseCase>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index);
+
+        AnsiConsole.MarkupLine($"[green]Drive #{settings.Index} removed from desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 12 - 0
RackPeek/Commands/Desktop/Drive/DesktopDriveRemoveSettings.cs

@@ -0,0 +1,12 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Drives;
+
+public class DesktopDriveRemoveSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+}

+ 31 - 0
RackPeek/Commands/Desktop/Drive/DesktopDriveSetCommand.cs

@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Drives;
+
+public class DesktopDriveSetCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopDriveSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopDriveSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateDesktopDriveUseCase>();
+
+        var drive = new Drive
+        {
+            Type = settings.Type,
+            Size = settings.Size
+        };
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index, drive);
+
+        AnsiConsole.MarkupLine($"[green]Drive #{settings.Index} updated on desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 18 - 0
RackPeek/Commands/Desktop/Drive/DesktopDriveSetSettings.cs

@@ -0,0 +1,18 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Drives;
+
+public class DesktopDriveSetSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+
+    [CommandOption("--type")]
+    public string? Type { get; set; }
+
+    [CommandOption("--size")]
+    public int? Size { get; set; }
+}

+ 31 - 0
RackPeek/Commands/Desktop/Gpu/DesktopGpuAddCommand.cs

@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Gpu;
+
+public class DesktopGpuAddCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopGpuAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopGpuAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddDesktopGpuUseCase>();
+
+        var gpu = new Domain.Resources.Hardware.Models.Gpu
+        {
+            Model = settings.Model,
+            Vram = settings.Vram
+        };
+
+        await useCase.ExecuteAsync(settings.DesktopName, gpu);
+
+        AnsiConsole.MarkupLine($"[green]GPU added to desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 15 - 0
RackPeek/Commands/Desktop/Gpu/DesktopGpuAddSettings.cs

@@ -0,0 +1,15 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Gpu;
+
+public class DesktopGpuAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandOption("--model")]
+    public string? Model { get; set; }
+
+    [CommandOption("--vram")]
+    public int? Vram { get; set; }
+}

+ 24 - 0
RackPeek/Commands/Desktop/Gpu/DesktopGpuRemoveCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Gpu;
+
+public class DesktopGpuRemoveCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopGpuRemoveSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopGpuRemoveSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RemoveDesktopGpuUseCase>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index);
+
+        AnsiConsole.MarkupLine($"[green]GPU #{settings.Index} removed from desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 12 - 0
RackPeek/Commands/Desktop/Gpu/DesktopGpuRemoveSettings.cs

@@ -0,0 +1,12 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Gpu;
+
+public class DesktopGpuRemoveSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+}

+ 31 - 0
RackPeek/Commands/Desktop/Gpu/DesktopGpuSetCommand.cs

@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Gpu;
+
+public class DesktopGpuSetCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopGpuSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopGpuSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateDesktopGpuUseCase>();
+
+        var gpu = new Domain.Resources.Hardware.Models.Gpu
+        {
+            Model = settings.Model,
+            Vram = settings.Vram
+        };
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index, gpu);
+
+        AnsiConsole.MarkupLine($"[green]GPU #{settings.Index} updated on desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 18 - 0
RackPeek/Commands/Desktop/Gpu/DesktopGpuSetSettings.cs

@@ -0,0 +1,18 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Gpu;
+
+public class DesktopGpuSetSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+
+    [CommandOption("--model")]
+    public string? Model { get; set; }
+
+    [CommandOption("--vram")]
+    public int? Vram { get; set; }
+}

+ 32 - 0
RackPeek/Commands/Desktop/Nic/DesktopNicAddCommand.cs

@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Nics;
+
+public class DesktopNicAddCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopNicAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopNicAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddDesktopNicUseCase>();
+
+        var nic = new Nic
+        {
+            Type = settings.Type,
+            Speed = settings.Speed,
+            Ports = settings.Ports
+        };
+
+        await useCase.ExecuteAsync(settings.DesktopName, nic);
+
+        AnsiConsole.MarkupLine($"[green]NIC added to desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 18 - 0
RackPeek/Commands/Desktop/Nic/DesktopNicAddSettings.cs

@@ -0,0 +1,18 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Nics;
+
+public class DesktopNicAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandOption("--type")]
+    public string? Type { get; set; }
+
+    [CommandOption("--speed")]
+    public int? Speed { get; set; }
+
+    [CommandOption("--ports")]
+    public int? Ports { get; set; }
+}

+ 24 - 0
RackPeek/Commands/Desktop/Nic/DesktopNicRemoveCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Nics;
+
+public class DesktopNicRemoveCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopNicRemoveSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopNicRemoveSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RemoveDesktopNicUseCase>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index);
+
+        AnsiConsole.MarkupLine($"[green]NIC #{settings.Index} removed from desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 12 - 0
RackPeek/Commands/Desktop/Nic/DesktopNicRemoveSettings.cs

@@ -0,0 +1,12 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Nics;
+
+public class DesktopNicRemoveSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+}

+ 32 - 0
RackPeek/Commands/Desktop/Nic/DesktopNicSetCommand.cs

@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Nics;
+
+public class DesktopNicSetCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopNicSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopNicSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateDesktopNicUseCase>();
+
+        var nic = new Nic
+        {
+            Type = settings.Type,
+            Speed = settings.Speed,
+            Ports = settings.Ports
+        };
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index, nic);
+
+        AnsiConsole.MarkupLine($"[green]NIC #{settings.Index} updated on desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 21 - 0
RackPeek/Commands/Desktop/Nic/DesktopNicSetSettings.cs

@@ -0,0 +1,21 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Nics;
+
+public class DesktopNicSetSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+
+    [CommandOption("--type")]
+    public string? Type { get; set; }
+
+    [CommandOption("--speed")]
+    public int? Speed { get; set; }
+
+    [CommandOption("--ports")]
+    public int? Ports { get; set; }
+}

+ 29 - 0
RackPeek/Commands/Ups/UpsAddCommand.cs

@@ -0,0 +1,29 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Ups;
+
+public class UpsAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<name>")] public string Name { get; set; } = default!;
+}
+
+public class UpsAddCommand(IServiceProvider provider)
+    : AsyncCommand<UpsAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        UpsAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddUpsUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine($"[green]UPS '{settings.Name}' added.[/]");
+        return 0;
+    }
+}

+ 29 - 0
RackPeek/Commands/Ups/UpsDeleteCommand.cs

@@ -0,0 +1,29 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Ups;
+
+public class UpsNameSettings : CommandSettings
+{
+    [CommandArgument(0, "<name>")] public string Name { get; set; } = default!;
+}
+
+public class UpsDeleteCommand(IServiceProvider provider)
+    : AsyncCommand<UpsNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        UpsNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DeleteUpsUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine($"[green]UPS '{settings.Name}' deleted.[/]");
+        return 0;
+    }
+}

+ 39 - 0
RackPeek/Commands/Ups/UpsDescribeCommand.cs

@@ -0,0 +1,39 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Ups;
+
+public class UpsDescribeCommand(IServiceProvider provider)
+    : AsyncCommand<UpsNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        UpsNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeUpsUseCase>();
+
+        var ups = await useCase.ExecuteAsync(settings.Name);
+
+        if (ups == null)
+        {
+            AnsiConsole.MarkupLine($"[red]UPS '{settings.Name}' not found.[/]");
+            return 1;
+        }
+
+        var grid = new Grid()
+            .AddColumn()
+            .AddColumn();
+
+        grid.AddRow("Name:", ups.Name);
+        grid.AddRow("Model:", ups.Model ?? "Unknown");
+        grid.AddRow("VA:", ups.Va?.ToString() ?? "Unknown");
+
+        AnsiConsole.Write(new Panel(grid).Header("UPS").Border(BoxBorder.Rounded));
+
+        return 0;
+    }
+}

+ 32 - 0
RackPeek/Commands/Ups/UpsGetByNameCommand.cs

@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Ups;
+
+public class UpsGetByNameCommand(IServiceProvider provider)
+    : AsyncCommand<UpsNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        UpsNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeUpsUseCase>();
+
+        var ups = await useCase.ExecuteAsync(settings.Name);
+
+        if (ups == null)
+        {
+            AnsiConsole.MarkupLine($"[red]UPS '{settings.Name}' not found.[/]");
+            return 1;
+        }
+
+        AnsiConsole.MarkupLine(
+            $"[green]{ups.Name}[/]  Model: {ups.Model ?? "Unknown"}, VA: {ups.Va?.ToString() ?? "Unknown"}");
+
+        return 0;
+    }
+}

+ 42 - 0
RackPeek/Commands/Ups/UpsGetCommand.cs

@@ -0,0 +1,42 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Reports;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Ups;
+
+public class UpsGetCommand(IServiceProvider provider)
+    : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpsHardwareReportUseCase>();
+
+        var report = await useCase.ExecuteAsync();
+
+        if (report.UpsUnits.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No UPS units found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("Model")
+            .AddColumn("VA");
+
+        foreach (var ups in report.UpsUnits)
+            table.AddRow(
+                ups.Name,
+                ups.Model,
+                ups.Va.ToString()
+            );
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}

+ 31 - 0
RackPeek/Commands/Ups/UpsSetCommand.cs

@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Commands.Server;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Ups;
+
+public class UpsSetSettings : ServerNameSettings
+{
+    [CommandOption("--model")] public string? Model { get; set; }
+    [CommandOption("--va")] public int? Va { get; set; }
+}
+
+public class UpsSetCommand(IServiceProvider provider)
+    : AsyncCommand<UpsSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        UpsSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateUpsUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name, settings.Model, settings.Va);
+
+        AnsiConsole.MarkupLine($"[green]UPS '{settings.Name}' updated.[/]");
+        return 0;
+    }
+}

+ 200 - 0
RackPeek/Program.cs

@@ -2,6 +2,12 @@
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using RackPeek.Commands;
+using RackPeek.Commands.AccessPoints;
+using RackPeek.Commands.Desktop;
+using RackPeek.Commands.Desktop.Cpus;
+using RackPeek.Commands.Desktop.Drives;
+using RackPeek.Commands.Desktop.Gpu;
+using RackPeek.Commands.Desktop.Nics;
 using RackPeek.Commands.Server;
 using RackPeek.Commands.Server.Cpus;
 using RackPeek.Commands.Server.Drives;
@@ -9,7 +15,10 @@ using RackPeek.Commands.Server.Gpu;
 using RackPeek.Commands.Server.Nics;
 using RackPeek.Commands.Switches;
 using RackPeek.Commands.Systems;
+using RackPeek.Commands.Ups;
 using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+using RackPeek.Domain.Resources.Hardware.Desktop;
 using RackPeek.Domain.Resources.Hardware.Reports;
 using RackPeek.Domain.Resources.Hardware.Server;
 using RackPeek.Domain.Resources.Hardware.Server.Cpu;
@@ -17,6 +26,7 @@ using RackPeek.Domain.Resources.Hardware.Server.Drive;
 using RackPeek.Domain.Resources.Hardware.Server.Gpu;
 using RackPeek.Domain.Resources.Hardware.Server.Nic;
 using RackPeek.Domain.Resources.Hardware.Switches;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
 using RackPeek.Domain.Resources.SystemResources;
 using RackPeek.Domain.Resources.SystemResources.UseCases;
 using RackPeek.Spectre;
@@ -183,6 +193,99 @@ public static class CliBootstrap
         services.AddScoped<SystemDeleteCommand>();
         services.AddScoped<SystemAddCommand>();
         services.AddScoped<SystemReportCommand>();
+        
+        // AccessPoint use cases
+        services.AddScoped<AddAccessPointUseCase>();
+        services.AddScoped<DeleteAccessPointUseCase>();
+        services.AddScoped<GetAccessPointUseCase>();
+        services.AddScoped<GetAccessPointsUseCase>();
+        services.AddScoped<UpdateAccessPointUseCase>();
+        services.AddScoped<DescribeAccessPointUseCase>();
+        
+        // AccessPoint commands
+        services.AddScoped<AccessPointAddCommand>();
+        services.AddScoped<AccessPointDeleteCommand>();
+        services.AddScoped<AccessPointDescribeCommand>();
+        services.AddScoped<AccessPointGetByNameCommand>();
+        services.AddScoped<AccessPointGetCommand>();
+        services.AddScoped<AccessPointSetCommand>();
+
+        // UPS use cases
+        services.AddScoped<AddUpsUseCase>();
+        services.AddScoped<DeleteUpsUseCase>();
+        services.AddScoped<GetUpsUnitUseCase>();
+        services.AddScoped<GetUpsUseCase>();
+        services.AddScoped<UpdateUpsUseCase>();
+        services.AddScoped<DescribeUpsUseCase>();
+
+        // UPS commands
+        services.AddScoped<UpsAddCommand>();
+        services.AddScoped<UpsDeleteCommand>();
+        services.AddScoped<UpsDescribeCommand>();
+        services.AddScoped<UpsGetByNameCommand>();
+        services.AddScoped<UpsGetCommand>();
+        services.AddScoped<UpsSetCommand>();
+        
+        // Desktop use cases
+        services.AddScoped<AddDesktopUseCase>();
+        services.AddScoped<DeleteDesktopUseCase>();
+        services.AddScoped<DescribeDesktopUseCase>();
+        services.AddScoped<GetDesktopUseCase>();
+        services.AddScoped<GetDesktopsUseCase>();
+        services.AddScoped<UpdateDesktopUseCase>();
+
+// Desktop CPU use cases
+        services.AddScoped<AddDesktopCpuUseCase>();
+        services.AddScoped<UpdateDesktopCpuUseCase>();
+        services.AddScoped<RemoveDesktopCpuUseCase>();
+
+// Desktop Drive use cases
+        services.AddScoped<AddDesktopDriveUseCase>();
+        services.AddScoped<UpdateDesktopDriveUseCase>();
+        services.AddScoped<RemoveDesktopDriveUseCase>();
+
+// Desktop GPU use cases
+        services.AddScoped<AddDesktopGpuUseCase>();
+        services.AddScoped<UpdateDesktopGpuUseCase>();
+        services.AddScoped<RemoveDesktopGpuUseCase>();
+
+// Desktop NIC use cases
+        services.AddScoped<AddDesktopNicUseCase>();
+        services.AddScoped<UpdateDesktopNicUseCase>();
+        services.AddScoped<RemoveDesktopNicUseCase>();
+
+// Desktop CRUD commands
+        services.AddScoped<DesktopAddCommand>();
+        services.AddScoped<DesktopDeleteCommand>();
+        services.AddScoped<DesktopDescribeCommand>();
+        services.AddScoped<DesktopGetByNameCommand>();
+        services.AddScoped<DesktopGetCommand>();
+        services.AddScoped<DesktopSetCommand>();
+
+// Desktop CPU commands
+        services.AddScoped<DesktopCpuAddCommand>();
+        services.AddScoped<DesktopCpuSetCommand>();
+        services.AddScoped<DesktopCpuRemoveCommand>();
+
+// Desktop Drive commands
+        services.AddScoped<DesktopDriveAddCommand>();
+        services.AddScoped<DesktopDriveSetCommand>();
+        services.AddScoped<DesktopDriveRemoveCommand>();
+
+// Desktop GPU commands
+        services.AddScoped<DesktopGpuAddCommand>();
+        services.AddScoped<DesktopGpuSetCommand>();
+        services.AddScoped<DesktopGpuRemoveCommand>();
+
+// Desktop NIC commands
+        services.AddScoped<DesktopNicAddCommand>();
+        services.AddScoped<DesktopNicSetCommand>();
+        services.AddScoped<DesktopNicRemoveCommand>();
+        
+
+
+
+        
 
         
         
@@ -326,6 +429,103 @@ public static class CliBootstrap
                         .WithDescription("Delete a system");
                 });
                 
+                config.AddBranch("accesspoints", ap =>
+                {
+                    ap.SetDescription("Manage access points");
+
+                    ap.AddCommand<AccessPointReportCommand>("summary")
+                        .WithDescription("Show access point hardware report");
+
+                    ap.AddCommand<AccessPointAddCommand>("add")
+                        .WithDescription("Add a new access point");
+
+                    ap.AddCommand<AccessPointGetCommand>("list")
+                        .WithDescription("List access points");
+
+                    ap.AddCommand<AccessPointGetByNameCommand>("get")
+                        .WithDescription("Get an access point by name");
+
+                    ap.AddCommand<AccessPointDescribeCommand>("describe")
+                        .WithDescription("Show detailed information about an access point");
+
+                    ap.AddCommand<AccessPointSetCommand>("set")
+                        .WithDescription("Update access point properties");
+
+                    ap.AddCommand<AccessPointDeleteCommand>("del")
+                        .WithDescription("Delete an access point");
+                });
+                
+                config.AddBranch("ups", ups =>
+                {
+                    ups.SetDescription("Manage UPS units");
+
+                    ups.AddCommand<UpsReportCommand>("summary")
+                        .WithDescription("Show UPS hardware report");
+
+                    ups.AddCommand<UpsAddCommand>("add")
+                        .WithDescription("Add a new UPS");
+
+                    ups.AddCommand<UpsGetCommand>("list")
+                        .WithDescription("List UPS units");
+
+                    ups.AddCommand<UpsGetByNameCommand>("get")
+                        .WithDescription("Get a UPS by name");
+
+                    ups.AddCommand<UpsDescribeCommand>("describe")
+                        .WithDescription("Show detailed information about a UPS");
+
+                    ups.AddCommand<UpsSetCommand>("set")
+                        .WithDescription("Update UPS properties");
+
+                    ups.AddCommand<UpsDeleteCommand>("del")
+                        .WithDescription("Delete a UPS");
+                });
+
+                config.AddBranch("desktops", desktops =>
+                {
+                    // CRUD
+                    desktops.AddCommand<DesktopAddCommand>("add");
+                    desktops.AddCommand<DesktopGetCommand>("list");
+                    desktops.AddCommand<DesktopGetByNameCommand>("get");
+                    desktops.AddCommand<DesktopDescribeCommand>("describe");
+                    desktops.AddCommand<DesktopSetCommand>("set");
+                    desktops.AddCommand<DesktopDeleteCommand>("del");
+
+                    // CPU
+                    desktops.AddBranch("cpu", cpu =>
+                    {
+                        cpu.AddCommand<DesktopCpuAddCommand>("add");
+                        cpu.AddCommand<DesktopCpuSetCommand>("set");
+                        cpu.AddCommand<DesktopCpuRemoveCommand>("del");
+                    });
+
+                    // Drives
+                    desktops.AddBranch("drive", drive =>
+                    {
+                        drive.AddCommand<DesktopDriveAddCommand>("add");
+                        drive.AddCommand<DesktopDriveSetCommand>("set");
+                        drive.AddCommand<DesktopDriveRemoveCommand>("del");
+                    });
+
+                    // GPUs
+                    desktops.AddBranch("gpu", gpu =>
+                    {
+                        gpu.AddCommand<DesktopGpuAddCommand>("add");
+                        gpu.AddCommand<DesktopGpuSetCommand>("set");
+                        gpu.AddCommand<DesktopGpuRemoveCommand>("del");
+                    });
+
+                    // NICs
+                    desktops.AddBranch("nic", nic =>
+                    {
+                        nic.AddCommand<DesktopNicAddCommand>("add");
+                        nic.AddCommand<DesktopNicSetCommand>("set");
+                        nic.AddCommand<DesktopNicRemoveCommand>("del");
+                    });
+                    
+                });
+
+                
                 // ----------------------------
                 // Reports (read-only summaries)
                 // ----------------------------

+ 48 - 0
Tests/Hardware/AccessPoints/AddAccessPointUseCaseTests.cs

@@ -0,0 +1,48 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+
+
+
+namespace Tests.Hardware.AccessPoints;
+
+public class AddAccessPointUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Adds_new_ap_when_not_exists()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ap01").Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new AddAccessPointUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync("ap01");
+
+        // Assert
+        await repo.Received(1).AddAsync(Arg.Is<AccessPoint>(ap =>
+            ap.Name == "ap01"
+        ));
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Throws_if_ap_already_exists()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ap01").Returns(new AccessPoint { Name = "ap01" });
+
+        var sut = new AddAccessPointUseCase(repo);
+
+        // Act
+        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
+            await sut.ExecuteAsync("ap01")
+        );
+
+        // Assert
+        Assert.Equal("Access point 'ap01' already exists.", ex.Message);
+        await repo.DidNotReceive().AddAsync(Arg.Any<AccessPoint>());
+    }
+}

+ 45 - 0
Tests/Hardware/AccessPoints/DeleteAccessPointUseCaseTests.cs

@@ -0,0 +1,45 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+
+
+namespace Tests.Hardware.AccessPoints;
+
+public class DeleteAccessPointUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Deletes_ap_when_exists()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ap01").Returns(new AccessPoint { Name = "ap01" });
+
+        var sut = new DeleteAccessPointUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync("ap01");
+
+        // Assert
+        await repo.Received(1).DeleteAsync("ap01");
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Throws_if_ap_not_found()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ap01").Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new DeleteAccessPointUseCase(repo);
+
+        // Act
+        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
+            await sut.ExecuteAsync("ap01")
+        );
+
+        // Assert
+        Assert.Equal("Access point 'ap01' not found.", ex.Message);
+        await repo.DidNotReceive().DeleteAsync(Arg.Any<string>());
+    }
+}

+ 50 - 0
Tests/Hardware/AccessPoints/DescribeAccessPointUseCaseTests.cs

@@ -0,0 +1,50 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+
+
+namespace Tests.Hardware.AccessPoints;
+
+public class DescribeAccessPointUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Returns_null_when_ap_not_found()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ap01").Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new DescribeAccessPointUseCase(repo);
+
+        // Act
+        var result = await sut.ExecuteAsync("ap01");
+
+        // Assert
+        Assert.Null(result);
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Returns_description_when_ap_exists()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ap01").Returns(new AccessPoint
+        {
+            Name = "ap01",
+            Model = "U6-Lite",
+            Speed = 1.2
+        });
+
+        var sut = new DescribeAccessPointUseCase(repo);
+
+        // Act
+        var result = await sut.ExecuteAsync("ap01");
+
+        // Assert
+        Assert.NotNull(result);
+        Assert.Equal("ap01", result.Name);
+        Assert.Equal("U6-Lite", result.Model);
+        Assert.Equal(1.2, result.Speed);
+    }
+}

+ 43 - 0
Tests/Hardware/AccessPoints/GetAccessPointUseCaseTests.cs

@@ -0,0 +1,43 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+
+namespace Tests.Hardware.AccessPoints;
+
+public class GetAccessPointUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Returns_ap_when_it_exists()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        var ap = new AccessPoint { Name = "ap01" };
+        repo.GetByNameAsync("ap01").Returns(ap);
+
+        var sut = new GetAccessPointUseCase(repo);
+
+        // Act
+        var result = await sut.ExecuteAsync("ap01");
+
+        // Assert
+        Assert.NotNull(result);
+        Assert.Same(ap, result);
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Returns_null_when_hardware_is_not_ap()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("node01").Returns(new Server { Name = "node01" });
+
+        var sut = new GetAccessPointUseCase(repo);
+
+        // Act
+        var result = await sut.ExecuteAsync("node01");
+
+        // Assert
+        Assert.Null(result);
+    }
+}

+ 51 - 0
Tests/Hardware/AccessPoints/GetAccessPointsUseCaseTest.cs

@@ -0,0 +1,51 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+
+
+namespace Tests.Hardware.AccessPoints;
+
+public class GetAccessPointsUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Returns_only_access_points()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetAllAsync().Returns([
+            new AccessPoint { Name = "ap01" },
+            new Server { Name = "node01" },
+            new AccessPoint { Name = "ap02" }
+        ]);
+
+        var sut = new GetAccessPointsUseCase(repo);
+
+        // Act
+        var result = await sut.ExecuteAsync();
+
+        // Assert
+        Assert.Equal(2, result.Count);
+        Assert.All(result, ap => Assert.IsType<AccessPoint>(ap));
+        Assert.Contains(result, ap => ap.Name == "ap01");
+        Assert.Contains(result, ap => ap.Name == "ap02");
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Returns_empty_list_when_no_access_points_exist()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetAllAsync().Returns([
+            new Server { Name = "node01" }
+        ]);
+
+        var sut = new GetAccessPointsUseCase(repo);
+
+        // Act
+        var result = await sut.ExecuteAsync();
+
+        // Assert
+        Assert.Empty(result);
+    }
+}

+ 87 - 0
Tests/Hardware/AccessPoints/UpdateAccessPointUseCaseTests.cs

@@ -0,0 +1,87 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+
+namespace Tests.Hardware.AccessPoints;
+
+public class UpdateAccessPointUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Throws_when_ap_not_found()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ap01").Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new UpdateAccessPointUseCase(repo);
+
+        // Act
+        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
+            await sut.ExecuteAsync("ap01")
+        );
+
+        // Assert
+        Assert.Equal("Access point 'ap01' not found.", ex.Message);
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<AccessPoint>());
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Updates_only_provided_fields()
+    {
+        // Arrange
+        var existing = new AccessPoint
+        {
+            Name = "ap01",
+            Model = "OldModel",
+            Speed = 1.0
+        };
+
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ap01").Returns(existing);
+
+        var sut = new UpdateAccessPointUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync(
+            "ap01",
+            model: "NewModel",
+            speed: 2.5
+        );
+
+        // Assert
+        await repo.Received(1).UpdateAsync(Arg.Is<AccessPoint>(ap =>
+            ap.Name == "ap01" &&
+            ap.Model == "NewModel" &&
+            ap.Speed == 2.5
+        ));
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Does_not_update_model_when_empty_or_whitespace()
+    {
+        // Arrange
+        var existing = new AccessPoint
+        {
+            Name = "ap01",
+            Model = "KeepMe",
+            Speed = 1.0
+        };
+
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ap01").Returns(existing);
+
+        var sut = new UpdateAccessPointUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync(
+            "ap01",
+            model: "   "
+        );
+
+        // Assert
+        await repo.Received(1).UpdateAsync(Arg.Is<AccessPoint>(ap =>
+            ap.Model == "KeepMe"
+        ));
+    }
+}

+ 31 - 0
Tests/Hardware/Desktop/AddDesktopUseCaseTests.cs

@@ -0,0 +1,31 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+
+public class AddDesktopUseCaseTests
+{
+    [Fact]
+    public async Task Adds_New_Desktop()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns((Hardware?)null);
+
+        var useCase = new AddDesktopUseCase(repo);
+
+        await useCase.ExecuteAsync("desk1");
+
+        await repo.Received().AddAsync(Arg.Is<Desktop>(d => d.Name == "desk1"));
+    }
+
+    [Fact]
+    public async Task Throws_If_Desktop_Exists()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns(new Desktop { Name = "desk1" });
+
+        var useCase = new AddDesktopUseCase(repo);
+
+        await Assert.ThrowsAsync<InvalidOperationException>(() => useCase.ExecuteAsync("desk1"));
+    }
+}

+ 31 - 0
Tests/Hardware/Desktop/DeleteDesktopUseCaseTests.cs

@@ -0,0 +1,31 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+
+public class DeleteDesktopUseCaseTests
+{
+    [Fact]
+    public async Task Deletes_Desktop()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns(new Desktop { Name = "desk1" });
+
+        var useCase = new DeleteDesktopUseCase(repo);
+
+        await useCase.ExecuteAsync("desk1");
+
+        await repo.Received().DeleteAsync("desk1");
+    }
+
+    [Fact]
+    public async Task Throws_If_Not_Found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns((Hardware?)null);
+
+        var useCase = new DeleteDesktopUseCase(repo);
+
+        await Assert.ThrowsAsync<InvalidOperationException>(() => useCase.ExecuteAsync("desk1"));
+    }
+}

+ 47 - 0
Tests/Hardware/Desktop/DescribeDesktopUseCaseTests.cs

@@ -0,0 +1,47 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+
+public class DescribeDesktopUseCaseTests
+{
+    [Fact]
+    public async Task Returns_Description()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns(new Desktop
+        {
+            Name = "desk1",
+            Model = "Optiplex",
+            Cpus = new() { new Cpu() },
+            Ram = new Ram { Size = 16, Mts = 2666 },
+            Drives = new() { new Drive() },
+            Nics = new() { new Nic() },
+            Gpus = new() { new Gpu() }
+        });
+
+        var useCase = new DescribeDesktopUseCase(repo);
+
+        var result = await useCase.ExecuteAsync("desk1");
+
+        Assert.NotNull(result);
+        Assert.Equal("desk1", result!.Name);
+        Assert.Equal(1, result.CpuCount);
+        Assert.Equal(1, result.DriveCount);
+        Assert.Equal(1, result.NicCount);
+        Assert.Equal(1, result.GpuCount);
+    }
+
+    [Fact]
+    public async Task Returns_Null_If_Not_Found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns((Hardware?)null);
+
+        var useCase = new DescribeDesktopUseCase(repo);
+
+        var result = await useCase.ExecuteAsync("desk1");
+
+        Assert.Null(result);
+    }
+}

+ 34 - 0
Tests/Hardware/Desktop/GetDesktopUseCaseTests.cs

@@ -0,0 +1,34 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+
+public class GetDesktopUseCaseTests
+{
+    [Fact]
+    public async Task Returns_Desktop()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns(new Desktop { Name = "desk1" });
+
+        var useCase = new GetDesktopUseCase(repo);
+
+        var result = await useCase.ExecuteAsync("desk1");
+
+        Assert.NotNull(result);
+        Assert.Equal("desk1", result!.Name);
+    }
+
+    [Fact]
+    public async Task Returns_Null_If_Not_Found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns((Hardware?)null);
+
+        var useCase = new GetDesktopUseCase(repo);
+
+        var result = await useCase.ExecuteAsync("desk1");
+
+        Assert.Null(result);
+    }
+}

+ 24 - 0
Tests/Hardware/Desktop/GetDesktopsUseCaseTests.cs

@@ -0,0 +1,24 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+
+public class GetDesktopsUseCaseTests
+{
+    [Fact]
+    public async Task Returns_All_Desktops()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetAllAsync().Returns(new Hardware[]
+        {
+            new Desktop { Name = "desk1" },
+            new Desktop { Name = "desk2" }
+        });
+
+        var useCase = new GetDesktopsUseCase(repo);
+
+        var result = await useCase.ExecuteAsync();
+
+        Assert.Equal(2, result.Count);
+    }
+}

+ 34 - 0
Tests/Hardware/Desktop/UpdateDesktopUseCaseTests.cs

@@ -0,0 +1,34 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+
+public class UpdateDesktopUseCaseTests
+{
+    [Fact]
+    public async Task Updates_Model()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        var desktop = new Desktop { Name = "desk1" };
+        repo.GetByNameAsync("desk1").Returns(desktop);
+
+        var useCase = new UpdateDesktopUseCase(repo);
+
+        await useCase.ExecuteAsync("desk1", "Optiplex");
+
+        Assert.Equal("Optiplex", desktop.Model);
+        await repo.Received().UpdateAsync(desktop);
+    }
+
+    [Fact]
+    public async Task Throws_If_Not_Found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns((Hardware?)null);
+
+        var useCase = new UpdateDesktopUseCase(repo);
+
+        await Assert.ThrowsAsync<InvalidOperationException>(() =>
+            useCase.ExecuteAsync("desk1", "Optiplex"));
+    }
+}

+ 40 - 0
Tests/Hardware/Ups/AddUpsUseCaseTests.cs

@@ -0,0 +1,40 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+namespace Tests.Hardware.Ups;
+
+public class AddUpsUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Adds_new_ups_when_not_exists()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new AddUpsUseCase(repo);
+
+        await sut.ExecuteAsync("ups01");
+
+        await repo.Received(1).AddAsync(Arg.Is<RackPeek.Domain.Resources.Hardware.Models.Ups>(u =>
+            u.Name == "ups01"
+        ));
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Throws_if_ups_already_exists()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns(new RackPeek.Domain.Resources.Hardware.Models.Ups { Name = "ups01" });
+
+        var sut = new AddUpsUseCase(repo);
+
+        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
+            await sut.ExecuteAsync("ups01")
+        );
+
+        Assert.Equal("UPS 'ups01' already exists.", ex.Message);
+        await repo.DidNotReceive().AddAsync(Arg.Any<RackPeek.Domain.Resources.Hardware.Models.Ups>());
+    }
+}

+ 38 - 0
Tests/Hardware/Ups/DeleteUpsUseCaseTests.cs

@@ -0,0 +1,38 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+namespace Tests.Hardware.Ups;
+
+public class DeleteUpsUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Deletes_ups_when_exists()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns(new RackPeek.Domain.Resources.Hardware.Models.Ups { Name = "ups01" });
+
+        var sut = new DeleteUpsUseCase(repo);
+
+        await sut.ExecuteAsync("ups01");
+
+        await repo.Received(1).DeleteAsync("ups01");
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Throws_if_ups_not_found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new DeleteUpsUseCase(repo);
+
+        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
+            await sut.ExecuteAsync("ups01")
+        );
+
+        Assert.Equal("UPS 'ups01' not found.", ex.Message);
+        await repo.DidNotReceive().DeleteAsync(Arg.Any<string>());
+    }
+}

+ 43 - 0
Tests/Hardware/Ups/DescribeUpsUseCaseTests.cs

@@ -0,0 +1,43 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+namespace Tests.Hardware.Ups;
+
+public class DescribeUpsUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Returns_null_when_ups_not_found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new DescribeUpsUseCase(repo);
+
+        var result = await sut.ExecuteAsync("ups01");
+
+        Assert.Null(result);
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Returns_description_when_ups_exists()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns(new RackPeek.Domain.Resources.Hardware.Models.Ups
+        {
+            Name = "ups01",
+            Model = "APC-1500",
+            Va = 1500
+        });
+
+        var sut = new DescribeUpsUseCase(repo);
+
+        var result = await sut.ExecuteAsync("ups01");
+
+        Assert.NotNull(result);
+        Assert.Equal("ups01", result.Name);
+        Assert.Equal("APC-1500", result.Model);
+        Assert.Equal(1500, result.Va);
+    }
+}

+ 37 - 0
Tests/Hardware/Ups/GetUpsUnitUseCaseTests.cs

@@ -0,0 +1,37 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+namespace Tests.Hardware.Ups;
+
+public class GetUpsUnitUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Returns_ups_when_it_exists()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        var ups = new RackPeek.Domain.Resources.Hardware.Models.Ups { Name = "ups01" };
+        repo.GetByNameAsync("ups01").Returns(ups);
+
+        var sut = new GetUpsUnitUseCase(repo);
+
+        var result = await sut.ExecuteAsync("ups01");
+
+        Assert.NotNull(result);
+        Assert.Same(ups, result);
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Returns_null_when_hardware_is_not_ups()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("node01").Returns(new Server { Name = "node01" });
+
+        var sut = new GetUpsUnitUseCase(repo);
+
+        var result = await sut.ExecuteAsync("node01");
+
+        Assert.Null(result);
+    }
+}

+ 44 - 0
Tests/Hardware/Ups/GetUpsUseCaseTests.cs

@@ -0,0 +1,44 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+namespace Tests.Hardware.Ups;
+
+public class GetUpsUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Returns_only_ups_units()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetAllAsync().Returns([
+            new RackPeek.Domain.Resources.Hardware.Models.Ups { Name = "ups01" },
+            new Server { Name = "node01" },
+            new RackPeek.Domain.Resources.Hardware.Models.Ups { Name = "ups02" }
+        ]);
+
+        var sut = new GetUpsUseCase(repo);
+
+        var result = await sut.ExecuteAsync();
+
+        Assert.Equal(2, result.Count);
+        Assert.All(result, u => Assert.IsType<RackPeek.Domain.Resources.Hardware.Models.Ups>(u));
+        Assert.Contains(result, u => u.Name == "ups01");
+        Assert.Contains(result, u => u.Name == "ups02");
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Returns_empty_list_when_no_ups_exist()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetAllAsync().Returns([
+            new Server { Name = "node01" }
+        ]);
+
+        var sut = new GetUpsUseCase(repo);
+
+        var result = await sut.ExecuteAsync();
+
+        Assert.Empty(result);
+    }
+}

+ 78 - 0
Tests/Hardware/Ups/UpdateUpsUseCaseTests.cs

@@ -0,0 +1,78 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+namespace Tests.Hardware.Ups;
+
+public class UpdateUpsUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Throws_when_ups_not_found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new UpdateUpsUseCase(repo);
+
+        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
+            await sut.ExecuteAsync("ups01")
+        );
+
+        Assert.Equal("UPS 'ups01' not found.", ex.Message);
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<RackPeek.Domain.Resources.Hardware.Models.Ups>());
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Updates_only_provided_fields()
+    {
+        var existing = new RackPeek.Domain.Resources.Hardware.Models.Ups
+        {
+            Name = "ups01",
+            Model = "OldModel",
+            Va = 1000
+        };
+
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns(existing);
+
+        var sut = new UpdateUpsUseCase(repo);
+
+        await sut.ExecuteAsync(
+            "ups01",
+            model: "NewModel",
+            va: 1500
+        );
+
+        await repo.Received(1).UpdateAsync(Arg.Is<RackPeek.Domain.Resources.Hardware.Models.Ups>(u =>
+            u.Name == "ups01" &&
+            u.Model == "NewModel" &&
+            u.Va == 1500
+        ));
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Does_not_update_model_when_empty_or_whitespace()
+    {
+        var existing = new RackPeek.Domain.Resources.Hardware.Models.Ups
+        {
+            Name = "ups01",
+            Model = "KeepMe",
+            Va = 1000
+        };
+
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns(existing);
+
+        var sut = new UpdateUpsUseCase(repo);
+
+        await sut.ExecuteAsync(
+            "ups01",
+            model: "   "
+        );
+
+        await repo.Received(1).UpdateAsync(Arg.Is<RackPeek.Domain.Resources.Hardware.Models.Ups>(u =>
+            u.Model == "KeepMe"
+        ));
+    }
+}