4
0
Эх сурвалжийг харах

Added editor config / linter / formatter rules / ssh / hosts exporters (#237)

* Added editor config / linter / formatter rules / ssh / hosts exporters

* fixed cli test pipeline

* Ran linter
Tim Jones 1 сар өмнө
parent
commit
1229629519
100 өөрчлөгдсөн 903 нэмэгдсэн , 1371 устгасан
  1. 52 0
      .editorconfig
  2. 11 1
      .github/workflows/test-cli.yml
  3. 8 0
      Directory.Build.props
  4. 3 5
      RackPeek.Domain/Api/InventoryRequest.cs
  5. 2 3
      RackPeek.Domain/Api/InventoryResponse.cs
  6. 25 40
      RackPeek.Domain/Api/UpsertInventoryUseCase.cs
  7. 4 7
      RackPeek.Domain/Helpers/ConflictException.cs
  8. 3 5
      RackPeek.Domain/Helpers/DeepClone.cs
  9. 21 52
      RackPeek.Domain/Helpers/Normalize.cs
  10. 4 7
      RackPeek.Domain/Helpers/NotFoundException.cs
  11. 18 35
      RackPeek.Domain/Helpers/ThrowIfInvalid.cs
  12. 2 3
      RackPeek.Domain/IConsoleEmulator.cs
  13. 2 3
      RackPeek.Domain/IUseCase.cs
  14. 39 49
      RackPeek.Domain/Persistence/HardwareRepository.cs
  15. 5 8
      RackPeek.Domain/Persistence/IResourceCollection.cs
  16. 41 70
      RackPeek.Domain/Persistence/ResourceCollectionMerger.cs
  17. 11 26
      RackPeek.Domain/Persistence/ServiceRepository.cs
  18. 18 36
      RackPeek.Domain/Persistence/SystemRepository.cs
  19. 15 22
      RackPeek.Domain/Persistence/Yaml/Converters.cs
  20. 6 17
      RackPeek.Domain/Persistence/Yaml/ITextFileStore.cs
  21. 7 14
      RackPeek.Domain/Persistence/Yaml/NotesStringYamlConverter.cs
  22. 20 24
      RackPeek.Domain/Persistence/Yaml/RackPeekConfigMigrationDeserializer.cs
  23. 10 20
      RackPeek.Domain/Persistence/Yaml/ResourceYamlMigrationService.cs
  24. 128 170
      RackPeek.Domain/Persistence/Yaml/YamlResourceCollection.cs
  25. 4 4
      RackPeek.Domain/RackPeek.Domain.csproj
  26. 2 3
      RackPeek.Domain/Resources/AccessPoints/AccessPoint.cs
  27. 4 6
      RackPeek.Domain/Resources/AccessPoints/AccessPointHardwareReport.cs
  28. 5 9
      RackPeek.Domain/Resources/AccessPoints/UpdateAccessPointUseCase.cs
  29. 3 5
      RackPeek.Domain/Resources/Desktops/DescribeDesktopUseCase.cs
  30. 3 4
      RackPeek.Domain/Resources/Desktops/Desktop.cs
  31. 6 10
      RackPeek.Domain/Resources/Desktops/DesktopHardwareReport.cs
  32. 11 25
      RackPeek.Domain/Resources/Desktops/UpdateDesktopUseCase.cs
  33. 6 9
      RackPeek.Domain/Resources/Firewalls/DescribeFirewallUseCase.cs
  34. 2 3
      RackPeek.Domain/Resources/Firewalls/Firewall.cs
  35. 7 11
      RackPeek.Domain/Resources/Firewalls/FirewallHardwareReport.cs
  36. 3 5
      RackPeek.Domain/Resources/Firewalls/UpdateFirewallUseCase.cs
  37. 10 17
      RackPeek.Domain/Resources/Hardware/GetHardwareSystemTreeUseCase.cs
  38. 7 11
      RackPeek.Domain/Resources/Hardware/GetHardwareUseCaseSummary.cs
  39. 2 3
      RackPeek.Domain/Resources/Hardware/Hardware.cs
  40. 4 7
      RackPeek.Domain/Resources/Hardware/IHardwareRepository.cs
  41. 2 3
      RackPeek.Domain/Resources/IResourceRepository.cs
  42. 3 5
      RackPeek.Domain/Resources/Laptops/DescribeLaptopUseCase.cs
  43. 2 3
      RackPeek.Domain/Resources/Laptops/Laptop.cs
  44. 5 8
      RackPeek.Domain/Resources/Laptops/LaptopHardwareReportUseCase.cs
  45. 11 25
      RackPeek.Domain/Resources/Laptops/UpdateLaptopUseCase.cs
  46. 28 40
      RackPeek.Domain/Resources/Resource.cs
  47. 6 9
      RackPeek.Domain/Resources/Routers/DescribeRouterUseCase.cs
  48. 2 3
      RackPeek.Domain/Resources/Routers/Router.cs
  49. 7 11
      RackPeek.Domain/Resources/Routers/RouterHardwareReport.cs
  50. 3 5
      RackPeek.Domain/Resources/Routers/UpdateRouterUseCase.cs
  51. 3 5
      RackPeek.Domain/Resources/Servers/DescribeServerUseCase.cs
  52. 2 3
      RackPeek.Domain/Resources/Servers/ICpuResource.cs
  53. 2 3
      RackPeek.Domain/Resources/Servers/IDriveResource.cs
  54. 2 3
      RackPeek.Domain/Resources/Servers/IGpuResource.cs
  55. 2 3
      RackPeek.Domain/Resources/Servers/INicResource.cs
  56. 2 3
      RackPeek.Domain/Resources/Servers/IPortResource.cs
  57. 2 3
      RackPeek.Domain/Resources/Servers/Server.cs
  58. 20 25
      RackPeek.Domain/Resources/Servers/ServerHardwareReport.cs
  59. 10 24
      RackPeek.Domain/Resources/Servers/UpdateServerUseCase.cs
  60. 2 3
      RackPeek.Domain/Resources/Services/IServiceRepository.cs
  61. 6 15
      RackPeek.Domain/Resources/Services/Networking/Cidr.cs
  62. 5 9
      RackPeek.Domain/Resources/Services/Networking/IpHelper.cs
  63. 6 11
      RackPeek.Domain/Resources/Services/Service.cs
  64. 5 14
      RackPeek.Domain/Resources/Services/UseCases/DescribeServiceUseCase.cs
  65. 6 9
      RackPeek.Domain/Resources/Services/UseCases/GetServiceSummaryUseCase.cs
  66. 10 19
      RackPeek.Domain/Resources/Services/UseCases/ServiceReportUseCase.cs
  67. 12 25
      RackPeek.Domain/Resources/Services/UseCases/ServiceSubnetsUseCase.cs
  68. 9 17
      RackPeek.Domain/Resources/Services/UseCases/UpdateServiceUseCase.cs
  69. 3 7
      RackPeek.Domain/Resources/SubResources/Cpu.cs
  70. 2 3
      RackPeek.Domain/Resources/SubResources/Drive.cs
  71. 2 3
      RackPeek.Domain/Resources/SubResources/Gpu.cs
  72. 2 3
      RackPeek.Domain/Resources/SubResources/Nic.cs
  73. 2 3
      RackPeek.Domain/Resources/SubResources/Port.cs
  74. 2 3
      RackPeek.Domain/Resources/SubResources/Ram.cs
  75. 6 9
      RackPeek.Domain/Resources/Switches/DescribeSwitchUseCase.cs
  76. 2 3
      RackPeek.Domain/Resources/Switches/Switch.cs
  77. 7 11
      RackPeek.Domain/Resources/Switches/SwitchHardwareReport.cs
  78. 3 5
      RackPeek.Domain/Resources/Switches/UpdateSwitchUseCase.cs
  79. 2 3
      RackPeek.Domain/Resources/SystemResources/ISystemRepository.cs
  80. 3 4
      RackPeek.Domain/Resources/SystemResources/SystemResource.cs
  81. 2 4
      RackPeek.Domain/Resources/SystemResources/UseCases/DescribeSystemUseCase.cs
  82. 4 7
      RackPeek.Domain/Resources/SystemResources/UseCases/GetSystemServiceTreeUseCase.cs
  83. 8 12
      RackPeek.Domain/Resources/SystemResources/UseCases/GetSystemSummaryUseCase.cs
  84. 4 7
      RackPeek.Domain/Resources/SystemResources/UseCases/SystemReportUseCase.cs
  85. 9 21
      RackPeek.Domain/Resources/SystemResources/UseCases/UpdateSystemUseCase.cs
  86. 3 5
      RackPeek.Domain/Resources/UpsUnits/DescribeUpsUseCase.cs
  87. 3 5
      RackPeek.Domain/Resources/UpsUnits/UpdateUpsUseCase.cs
  88. 2 3
      RackPeek.Domain/Resources/UpsUnits/Ups.cs
  89. 5 8
      RackPeek.Domain/Resources/UpsUnits/UpsHardwareReport.cs
  90. 2 3
      RackPeek.Domain/RpkConstants.cs
  91. 15 19
      RackPeek.Domain/ServiceCollectionExtensions.cs
  92. 6 11
      RackPeek.Domain/UseCases/AddResourceUseCase.cs
  93. 49 86
      RackPeek.Domain/UseCases/Ansible/AnsibleInventoryGenerator.cs
  94. 4 6
      RackPeek.Domain/UseCases/Ansible/AnsibleInventoryGeneratorUseCase.cs
  95. 6 9
      RackPeek.Domain/UseCases/CloneAccessPointUseCase.cs
  96. 7 11
      RackPeek.Domain/UseCases/Cpus/AddCpuUseCase.cs
  97. 6 9
      RackPeek.Domain/UseCases/Cpus/RemoveCpuUseCase.cs
  98. 8 10
      RackPeek.Domain/UseCases/Cpus/UpdateCpuUseCase.cs
  99. 8 12
      RackPeek.Domain/UseCases/DeleteResourceUseCase.cs
  100. 7 11
      RackPeek.Domain/UseCases/Drives/AddDriveUseCase.cs

+ 52 - 0
.editorconfig

@@ -0,0 +1,52 @@
+root = true
+
+[*.cs]
+
+###############
+# Formatting  #
+###############
+
+indent_style = space
+indent_size = 4
+tab_width = 4
+
+end_of_line = lf
+insert_final_newline = true
+
+###############
+# C# Style    #
+###############
+
+csharp_new_line_before_open_brace = all:error
+csharp_indent_case_contents = true:error
+csharp_indent_switch_labels = true:error
+
+###############
+# var usage   #
+###############
+
+csharp_style_var_for_built_in_types = true:error
+csharp_style_var_when_type_is_apparent = true:error
+csharp_style_var_elsewhere = false:error
+csharp_style_var_in_deconstruction = true:error
+
+###############
+# Expression-bodied members
+###############
+
+csharp_style_expression_bodied_methods = when_on_single_line:error
+csharp_style_expression_bodied_properties = when_on_single_line:error
+
+###############
+# Naming rules
+###############
+
+dotnet_naming_rule.private_fields_should_be_camel_case.severity = error
+dotnet_naming_rule.private_fields_should_be_camel_case.symbols = private_fields
+dotnet_naming_rule.private_fields_should_be_camel_case.style = camel_case_style
+
+dotnet_naming_symbols.private_fields.applicable_kinds = field
+dotnet_naming_symbols.private_fields.applicable_accessibilities = private
+
+dotnet_naming_style.camel_case_style.capitalization = camel_case
+dotnet_naming_style.camel_case_style.required_prefix = _

+ 11 - 1
.github/workflows/test-cli.yml

@@ -7,6 +7,7 @@ on:
 jobs:
   build:
     runs-on: ubuntu-latest
+
     steps:
       - name: Checkout
         uses: actions/checkout@v3
@@ -16,5 +17,14 @@ jobs:
         with:
           dotnet-version: 10.0.x
 
+      - name: Restore
+        run: dotnet restore
+
+      - name: Check Formatting
+        run: dotnet format --verify-no-changes
+
+      - name: Build
+        run: dotnet build --no-restore --configuration Release
+
       - name: Test
-        run: dotnet test Tests --verbosity normal
+        run: dotnet test Tests --no-build --configuration Release --verbosity normal

+ 8 - 0
Directory.Build.props

@@ -0,0 +1,8 @@
+<Project>
+    <PropertyGroup>
+        <EnableNETAnalyzers>true</EnableNETAnalyzers>
+        <AnalysisLevel>latest</AnalysisLevel>
+        <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
+        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    </PropertyGroup>
+</Project>

+ 3 - 5
RackPeek.Domain/Api/InventoryRequest.cs

@@ -1,13 +1,11 @@
 using RackPeek.Domain.Persistence;
-using RackPeek.Domain.Persistence.Yaml;
 
 namespace RackPeek.Domain.Api;
 
-public class ImportYamlRequest
-{
+public class ImportYamlRequest {
     public string? Yaml { get; set; }
-    public object? Json { get; set; } 
+    public object? Json { get; set; }
     public MergeMode Mode { get; set; } = MergeMode.Merge;
 
     public bool DryRun { get; set; } = false;
-}
+}

+ 2 - 3
RackPeek.Domain/Api/InventoryResponse.cs

@@ -1,7 +1,6 @@
 namespace RackPeek.Domain.Api;
 
-public class ImportYamlResponse
-{
+public class ImportYamlResponse {
     public List<string> Added { get; set; } = new();
     public List<string> Updated { get; set; } = new();
     public List<string> Replaced { get; set; } = new();
@@ -11,4 +10,4 @@ public class ImportYamlResponse
 
     public Dictionary<string, string> NewYaml { get; set; }
         = new(StringComparer.OrdinalIgnoreCase);
-}
+}

+ 25 - 40
RackPeek.Domain/Api/UpsertInventoryUseCase.cs

@@ -2,8 +2,8 @@ using System.ComponentModel.DataAnnotations;
 using System.Text.Json;
 using System.Text.Json.Serialization;
 using RackPeek.Domain.Persistence;
-using RackPeek.Domain.Resources;
 using RackPeek.Domain.Persistence.Yaml;
+using RackPeek.Domain.Resources;
 using YamlDotNet.Serialization;
 using YamlDotNet.Serialization.NamingConventions;
 
@@ -12,10 +12,8 @@ namespace RackPeek.Domain.Api;
 public class UpsertInventoryUseCase(
     IResourceCollection repo,
     IResourceYamlMigrationService migrationService)
-    : IUseCase
-{
-    private static readonly JsonSerializerOptions JsonOptions = new()
-    {
+    : IUseCase {
+    private static readonly JsonSerializerOptions _jsonOptions = new() {
         PropertyNameCaseInsensitive = true,
         WriteIndented = false,
         DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
@@ -23,39 +21,36 @@ public class UpsertInventoryUseCase(
         TypeInfoResolver = ResourcePolymorphismResolver.Create()
     };
 
-    public async Task<ImportYamlResponse> ExecuteAsync(ImportYamlRequest request)
-    {
+    public async Task<ImportYamlResponse> ExecuteAsync(ImportYamlRequest request) {
         if (request == null)
             throw new ValidationException("Invalid request.");
 
         if (string.IsNullOrWhiteSpace(request.Yaml) && request.Json == null)
             throw new ValidationException("Either 'yaml' or 'json' must be provided.");
-        
+
         if (!string.IsNullOrWhiteSpace(request.Yaml) && request.Json != null)
             throw new ValidationException("Provide either 'yaml' or 'json', not both.");
-        
-        
+
+
         YamlRoot incomingRoot;
         string yamlInput;
 
-        if (!string.IsNullOrWhiteSpace(request.Yaml))
-        {
+        if (!string.IsNullOrWhiteSpace(request.Yaml)) {
             yamlInput = request.Yaml!;
             incomingRoot = await migrationService.DeserializeAsync(yamlInput)
                            ?? throw new ValidationException("Invalid YAML structure.");
         }
-        else
-        {
+        else {
             if (request.Json is not JsonElement element)
                 throw new ValidationException("Invalid JSON payload.");
-            
+
             var rawJson = element.GetRawText();
             incomingRoot = JsonSerializer.Deserialize<YamlRoot>(
                                rawJson,
-                               JsonOptions)
+                               _jsonOptions)
                            ?? throw new ValidationException("Invalid JSON structure.");
             // Generate YAML only for persistence layer
-            var yamlSerializer = new SerializerBuilder()
+            ISerializer yamlSerializer = new SerializerBuilder()
                 .WithNamingConvention(CamelCaseNamingConvention.Instance)
                 .WithTypeConverter(new StorageSizeYamlConverter())
                 .WithTypeConverter(new NotesStringYamlConverter())
@@ -72,20 +67,20 @@ public class UpsertInventoryUseCase(
 
         // 2️Compute Diff
 
-        var incomingResources = incomingRoot.Resources;
-        var currentResources = await repo.GetAllOfTypeAsync<Resource>();
+        List<Resource>? incomingResources = incomingRoot.Resources;
+        IReadOnlyList<Resource> currentResources = await repo.GetAllOfTypeAsync<Resource>();
 
-        var duplicate = incomingResources
+        IGrouping<string, Resource>? duplicate = incomingResources
             .GroupBy(r => r.Name, StringComparer.OrdinalIgnoreCase)
             .FirstOrDefault(g => g.Count() > 1);
 
         if (duplicate != null)
             throw new ValidationException($"Duplicate resource name: {duplicate.Key}");
-        
+
         var currentDict = currentResources
             .ToDictionary(r => r.Name, StringComparer.OrdinalIgnoreCase);
 
-        var serializerDiff = new SerializerBuilder()
+        ISerializer serializerDiff = new SerializerBuilder()
             .WithNamingConvention(CamelCaseNamingConvention.Instance)
             .ConfigureDefaultValuesHandling(
                 DefaultValuesHandling.OmitNull |
@@ -98,7 +93,7 @@ public class UpsertInventoryUseCase(
                 r => serializerDiff.Serialize(r),
                 StringComparer.OrdinalIgnoreCase);
 
-        var mergedResources = ResourceCollectionMerger.Merge(
+        List<Resource> mergedResources = ResourceCollectionMerger.Merge(
             currentResources,
             incomingResources,
             request.Mode);
@@ -108,16 +103,14 @@ public class UpsertInventoryUseCase(
 
         var response = new ImportYamlResponse();
 
-        foreach (var incoming in incomingResources)
-        {
-            if (!mergedDict.TryGetValue(incoming.Name, out var merged))
+        foreach (Resource incoming in incomingResources) {
+            if (!mergedDict.TryGetValue(incoming.Name, out Resource? merged))
                 continue;
 
             var newYaml = serializerDiff.Serialize(merged);
             response.NewYaml[incoming.Name] = newYaml;
 
-            if (!currentDict.ContainsKey(incoming.Name))
-            {
+            if (!currentDict.ContainsKey(incoming.Name)) {
                 response.Added.Add(incoming.Name);
                 continue;
             }
@@ -125,24 +118,16 @@ public class UpsertInventoryUseCase(
             var oldYaml = oldSnapshots[incoming.Name];
             response.OldYaml[incoming.Name] = oldYaml;
 
-            var existing = currentDict[incoming.Name];
+            Resource existing = currentDict[incoming.Name];
 
             if (request.Mode == MergeMode.Replace ||
                 existing.GetType() != incoming.GetType())
-            {
                 response.Replaced.Add(incoming.Name);
-            }
-            else if (oldYaml != newYaml)
-            {
-                response.Updated.Add(incoming.Name);
-            }
+            else if (oldYaml != newYaml) response.Updated.Add(incoming.Name);
         }
 
-        if (!request.DryRun)
-        {
-            await repo.Merge(yamlInput, request.Mode);
-        }
+        if (!request.DryRun) await repo.Merge(yamlInput, request.Mode);
 
         return response;
     }
-}
+}

+ 4 - 7
RackPeek.Domain/Helpers/ConflictException.cs

@@ -1,14 +1,11 @@
 namespace RackPeek.Domain.Helpers;
 
-public sealed class ConflictException : Exception
-{
+public sealed class ConflictException : Exception {
     public ConflictException(string message)
-        : base(message)
-    {
+        : base(message) {
     }
 
     public ConflictException(string message, Exception innerException)
-        : base(message, innerException)
-    {
+        : base(message, innerException) {
     }
-}
+}

+ 3 - 5
RackPeek.Domain/Helpers/DeepClone.cs

@@ -2,11 +2,9 @@ using System.Text.Json;
 
 namespace RackPeek.Domain.Helpers;
 
-public static class Clone
-{
-    public static T DeepClone<T>(T obj)
-    {
+public static class Clone {
+    public static T DeepClone<T>(T obj) {
         var json = JsonSerializer.Serialize(obj);
         return JsonSerializer.Deserialize<T>(json)!;
     }
-}
+}

+ 21 - 52
RackPeek.Domain/Helpers/Normalize.cs

@@ -1,54 +1,23 @@
 namespace RackPeek.Domain.Helpers;
 
-public static class Normalize
-{
-    public static string DriveType(string value)
-    {
-        return value.Trim().ToLowerInvariant();
-    }
-
-    public static string NicType(string value)
-    {
-        return value.Trim().ToLowerInvariant();
-    }
-
-    public static string SystemType(string value)
-    {
-        return value.Trim().ToLowerInvariant();
-    }
-
-    public static string SystemName(string name)
-    {
-        return name.Trim();
-    }
-
-    public static string ServiceName(string name)
-    {
-        return name.Trim();
-    }
-
-    public static string HardwareName(string name)
-    {
-        return name.Trim();
-    }
-
-    public static string ResourceName(string name)
-    {
-        return name.Trim();
-    }
-
-    public static string Tag(string name)
-    {
-        return name.Trim();
-    }
-
-    public static string LabelKey(string key)
-    {
-        return key.Trim();
-    }
-
-    public static string LabelValue(string value)
-    {
-        return value.Trim();
-    }
-}
+public static class Normalize {
+    public static string DriveType(string value) => value.Trim().ToLowerInvariant();
+
+    public static string NicType(string value) => value.Trim().ToLowerInvariant();
+
+    public static string SystemType(string value) => value.Trim().ToLowerInvariant();
+
+    public static string SystemName(string name) => name.Trim();
+
+    public static string ServiceName(string name) => name.Trim();
+
+    public static string HardwareName(string name) => name.Trim();
+
+    public static string ResourceName(string name) => name.Trim();
+
+    public static string Tag(string name) => name.Trim();
+
+    public static string LabelKey(string key) => key.Trim();
+
+    public static string LabelValue(string value) => value.Trim();
+}

+ 4 - 7
RackPeek.Domain/Helpers/NotFoundException.cs

@@ -1,14 +1,11 @@
 namespace RackPeek.Domain.Helpers;
 
-public sealed class NotFoundException : Exception
-{
+public sealed class NotFoundException : Exception {
     public NotFoundException(string message)
-        : base(message)
-    {
+        : base(message) {
     }
 
     public NotFoundException(string message, Exception innerException)
-        : base(message, innerException)
-    {
+        : base(message, innerException) {
     }
-}
+}

+ 18 - 35
RackPeek.Domain/Helpers/ThrowIfInvalid.cs

@@ -4,29 +4,24 @@ using RackPeek.Domain.Resources.SystemResources;
 
 namespace RackPeek.Domain.Helpers;
 
-public static class ThrowIfInvalid
-{
-    public static void ResourceName(string name)
-    {
+public static class ThrowIfInvalid {
+    public static void ResourceName(string name) {
         if (string.IsNullOrWhiteSpace(name)) throw new ValidationException("Name is required.");
 
         if (name.Length > 50) throw new ValidationException("Name is too long.");
     }
 
-    public static void LabelKey(string key)
-    {
+    public static void LabelKey(string key) {
         if (string.IsNullOrWhiteSpace(key)) throw new ValidationException("Label key is required.");
         if (key.Length > 50) throw new ValidationException("Label key is too long.");
     }
 
-    public static void LabelValue(string value)
-    {
+    public static void LabelValue(string value) {
         if (string.IsNullOrWhiteSpace(value)) throw new ValidationException("Label value is required.");
         if (value.Length > 200) throw new ValidationException("Label value is too long.");
     }
 
-    public static void AccessPointModelName(string name)
-    {
+    public static void AccessPointModelName(string name) {
         if (string.IsNullOrWhiteSpace(name))
             throw new ValidationException("Model name is required.");
 
@@ -34,15 +29,13 @@ public static class ThrowIfInvalid
             throw new ValidationException("Model name is too long.");
     }
 
-    public static void RamGb(double? value)
-    {
+    public static void RamGb(double? value) {
         if (value is null) throw new ValidationException("RAM value must be specified.");
 
         if (value < 0) throw new ValidationException("RAM value must be a non negative number of gigabytes.");
     }
 
-    public static void SystemType(string systemType)
-    {
+    public static void SystemType(string systemType) {
         if (string.IsNullOrWhiteSpace(systemType)) throw new ValidationException("System type is required.");
 
         var normalized = systemType.Trim().ToLowerInvariant();
@@ -58,8 +51,7 @@ public static class ThrowIfInvalid
         throw new ValidationException(message);
     }
 
-    private static IEnumerable<string> GetSystemTypeSuggestions(string input)
-    {
+    private static IEnumerable<string> GetSystemTypeSuggestions(string input) {
         return SystemResource.ValidSystemTypes.Select(type => new { Type = type, Score = SimilarityScore(input, type) })
             .Where(x => x.Score >= 0.5)
             .OrderByDescending(x => x.Score)
@@ -69,8 +61,7 @@ public static class ThrowIfInvalid
 
     #region Nics
 
-    public static void NicType(string nicType)
-    {
+    public static void NicType(string nicType) {
         if (string.IsNullOrWhiteSpace(nicType)) throw new ValidationException("NIC type is required.");
 
         var normalized = nicType.Trim().ToLowerInvariant();
@@ -86,8 +77,7 @@ public static class ThrowIfInvalid
         throw new ValidationException(message);
     }
 
-    private static IEnumerable<string> GetNicTypeSuggestions(string input)
-    {
+    private static IEnumerable<string> GetNicTypeSuggestions(string input) {
         return Nic.ValidNicTypes.Select(type => new { Type = type, Score = SimilarityScore(input, type) })
             .Where(x => x.Score >= 0.5)
             .OrderByDescending(x => x.Score)
@@ -95,8 +85,7 @@ public static class ThrowIfInvalid
             .Select(x => x.Type);
     }
 
-    private static double SimilarityScore(string a, string b)
-    {
+    private static double SimilarityScore(string a, string b) {
         if (a == b) return 1.0;
 
         if (b.StartsWith(a) || a.StartsWith(b)) return 0.9;
@@ -105,21 +94,18 @@ public static class ThrowIfInvalid
         return (double)commonChars / Math.Max(a.Length, b.Length);
     }
 
-    public static void NicSpeed(double speed)
-    {
+    public static void NicSpeed(double speed) {
         if (speed < 0) throw new ValidationException("NIC speed must be a non negative number of gigabits per second.");
     }
 
-    public static void NetworkSpeed(double speed)
-    {
+    public static void NetworkSpeed(double speed) {
         if (speed < 0)
             throw new ValidationException(
                 "Network speed must be a non negative number of gigabits per second.");
     }
 
 
-    public static void NicPorts(int ports)
-    {
+    public static void NicPorts(int ports) {
         if (ports < 0) throw new ValidationException("NIC port count must be a non negative integer.");
     }
 
@@ -127,8 +113,7 @@ public static class ThrowIfInvalid
 
     #region Drives
 
-    public static void DriveType(string driveType)
-    {
+    public static void DriveType(string driveType) {
         if (string.IsNullOrWhiteSpace(driveType)) throw new ValidationException("Drive type is required.");
 
         var normalized = driveType.Trim().ToLowerInvariant();
@@ -144,8 +129,7 @@ public static class ThrowIfInvalid
         throw new ValidationException(message);
     }
 
-    private static IEnumerable<string> GetDriveTypeSuggestions(string input)
-    {
+    private static IEnumerable<string> GetDriveTypeSuggestions(string input) {
         return Drive.ValidDriveTypes.Select(type => new { Type = type, Score = SimilarityScore(input, type) })
             .Where(x => x.Score >= 0.5)
             .OrderByDescending(x => x.Score)
@@ -153,10 +137,9 @@ public static class ThrowIfInvalid
             .Select(x => x.Type);
     }
 
-    public static void DriveSize(int size)
-    {
+    public static void DriveSize(int size) {
         if (size < 0) throw new ValidationException("Drive size value must be a non negative number of gigabytes.");
     }
 
     #endregion
-}
+}

+ 2 - 3
RackPeek.Domain/IConsoleEmulator.cs

@@ -1,6 +1,5 @@
 namespace RackPeek.Domain;
 
-public interface IConsoleEmulator
-{
+public interface IConsoleEmulator {
     public Task<string> Execute(string input);
-}
+}

+ 2 - 3
RackPeek.Domain/IUseCase.cs

@@ -1,5 +1,4 @@
 namespace RackPeek.Domain;
 
-public interface IUseCase
-{
-}
+public interface IUseCase {
+}

+ 39 - 49
RackPeek.Domain/Persistence/HardwareRepository.cs

@@ -1,78 +1,68 @@
 using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Services;
+using RackPeek.Domain.Resources.SystemResources;
 
 namespace RackPeek.Domain.Persistence;
 
-public class YamlHardwareRepository(IResourceCollection resources) : IHardwareRepository
-{
-    public Task<int> GetCountAsync()
-    {
-        return Task.FromResult(resources.HardwareResources.Count);
-    }
+public class YamlHardwareRepository(IResourceCollection resources) : IHardwareRepository {
+    public Task<int> GetCountAsync() => Task.FromResult(resources.HardwareResources.Count);
 
-    public Task<Dictionary<string, int>> GetKindCountAsync()
-    {
+    public Task<Dictionary<string, int>> GetKindCountAsync() {
         return Task.FromResult(resources.HardwareResources
             .GroupBy(h => h.Kind)
             .ToDictionary(k => k.Key, v => v.Count()));
     }
 
-    public Task<List<HardwareTree>> GetTreeAsync()
-    {
+    public Task<List<HardwareTree>> GetTreeAsync() {
         var hardwareTree = new List<HardwareTree>();
-        
-            var systemGroups = resources.SystemResources
-                .Where(s => s.RunsOn.Count != 0)
-                .SelectMany(
-                    s => s.RunsOn,
-                    (system, hardwareName) => new
-                    {
-                        Hardware = hardwareName.Trim(),
-                        System = system
-                    })
-                .GroupBy(x => x.Hardware, StringComparer.OrdinalIgnoreCase)
-                .ToDictionary(
-                    g => g.Key,
-                    g => g.Select(x => x.System).ToList(),
-                    StringComparer.OrdinalIgnoreCase);
 
-            var serviceGroups = resources.ServiceResources
-                .Where(s => s.RunsOn.Count != 0)
-                .SelectMany(
-                    s => s.RunsOn,
-                    (service, systemName) => new
-                    {
-                        System = systemName.Trim(),
-                        Service = service
-                    })
-                .GroupBy(x => x.System, StringComparer.OrdinalIgnoreCase)
-                .ToDictionary(
-                    g => g.Key,
-                    g => g.Select(x => x.Service).ToList(),
-                    StringComparer.OrdinalIgnoreCase);
+        var systemGroups = resources.SystemResources
+            .Where(s => s.RunsOn.Count != 0)
+            .SelectMany(
+                s => s.RunsOn,
+                (system, hardwareName) => new {
+                    Hardware = hardwareName.Trim(),
+                    System = system
+                })
+            .GroupBy(x => x.Hardware, StringComparer.OrdinalIgnoreCase)
+            .ToDictionary(
+                g => g.Key,
+                g => g.Select(x => x.System).ToList(),
+                StringComparer.OrdinalIgnoreCase);
+
+        var serviceGroups = resources.ServiceResources
+            .Where(s => s.RunsOn.Count != 0)
+            .SelectMany(
+                s => s.RunsOn,
+                (service, systemName) => new {
+                    System = systemName.Trim(),
+                    Service = service
+                })
+            .GroupBy(x => x.System, StringComparer.OrdinalIgnoreCase)
+            .ToDictionary(
+                g => g.Key,
+                g => g.Select(x => x.Service).ToList(),
+                StringComparer.OrdinalIgnoreCase);
 
-        foreach (var hardware in resources.HardwareResources)
-        {
+        foreach (Hardware hardware in resources.HardwareResources) {
             var systems = new List<SystemTree>();
             var hardwareKey = hardware.Name.Trim();
 
-            if (systemGroups.TryGetValue(hardwareKey, out var systemResources))
-                foreach (var system in systemResources)
-                {
+            if (systemGroups.TryGetValue(hardwareKey, out List<SystemResource>? systemResources))
+                foreach (SystemResource system in systemResources) {
                     var services = new List<string>();
                     var systemKey = system.Name.Trim();
 
-                    if (serviceGroups.TryGetValue(systemKey, out var serviceResources))
+                    if (serviceGroups.TryGetValue(systemKey, out List<Service>? serviceResources))
                         services.AddRange(serviceResources.Select(s => s.Name));
 
-                    systems.Add(new SystemTree
-                    {
+                    systems.Add(new SystemTree {
                         SystemName = system.Name,
                         Services = services
                     });
                 }
 
-            hardwareTree.Add(new HardwareTree
-            {
+            hardwareTree.Add(new HardwareTree {
                 Kind = hardware.Kind,
                 HardwareName = hardware.Name,
                 Systems = systems

+ 5 - 8
RackPeek.Domain/Persistence/IResourceCollection.cs

@@ -5,8 +5,7 @@ using RackPeek.Domain.Resources.SystemResources;
 
 namespace RackPeek.Domain.Persistence;
 
-public interface IResourceCollection
-{
+public interface IResourceCollection {
     IReadOnlyList<Hardware> HardwareResources { get; }
     IReadOnlyList<SystemResource> SystemResources { get; }
     IReadOnlyList<Service> ServiceResources { get; }
@@ -19,23 +18,21 @@ public interface IResourceCollection
 
     Resource? GetByName(string name);
     Task<bool> Exists(string name);
-    
+
     Task<string?> GetKind(string? name);
 
 
     Task LoadAsync(); // required for WASM startup
     Task<IReadOnlyList<Resource>> GetByTagAsync(string name);
     public Task<Dictionary<string, int>> GetTagsAsync();
-    
+
     Task<IReadOnlyList<(Resource, string)>> GetByLabelAsync(string name);
     public Task<Dictionary<string, int>> GetLabelsAsync();
-    
+
     Task<IReadOnlyList<(Resource, string)>> GetResourceIpsAsync();
 
     Task<IReadOnlyList<T>> GetAllOfTypeAsync<T>();
     Task<IReadOnlyList<Resource>> GetDependantsAsync(string name);
 
     Task Merge(string incomingYaml, MergeMode mode);
-
-
-}
+}

+ 41 - 70
RackPeek.Domain/Persistence/ResourceCollectionMerger.cs

@@ -1,8 +1,9 @@
-using RackPeek.Domain.Resources;
+using System.Collections;
 using System.Reflection;
 using System.Text.Json;
 using System.Text.Json.Serialization;
 using System.Text.Json.Serialization.Metadata;
+using RackPeek.Domain.Resources;
 using RackPeek.Domain.Resources.AccessPoints;
 using RackPeek.Domain.Resources.Desktops;
 using RackPeek.Domain.Resources.Firewalls;
@@ -16,16 +17,13 @@ using RackPeek.Domain.Resources.UpsUnits;
 
 namespace RackPeek.Domain.Persistence;
 
-public enum MergeMode
-{
+public enum MergeMode {
     Replace,
     Merge
 }
 
-public static class ResourceCollectionMerger
-{
-    private static readonly JsonSerializerOptions CloneJsonOptions = new()
-    {
+public static class ResourceCollectionMerger {
+    private static readonly JsonSerializerOptions _cloneJsonOptions = new() {
         PropertyNameCaseInsensitive = true,
         WriteIndented = false,
         DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
@@ -36,46 +34,39 @@ public static class ResourceCollectionMerger
     public static List<Resource> Merge(
         IEnumerable<Resource> original,
         IEnumerable<Resource> incoming,
-        MergeMode mode)
-    {
-        var originalClone = DeepCloneList(original);
-        var incomingClone = DeepCloneList(incoming);
+        MergeMode mode) {
+        List<Resource> originalClone = DeepCloneList(original);
+        List<Resource> incomingClone = DeepCloneList(incoming);
 
         var result = originalClone.ToDictionary(r => r.Name, r => r, StringComparer.OrdinalIgnoreCase);
 
-        foreach (var newResource in incomingClone)
-        {
-            if (!result.TryGetValue(newResource.Name, out var existing))
-            {
+        foreach (Resource newResource in incomingClone) {
+            if (!result.TryGetValue(newResource.Name, out Resource? existing)) {
                 result[newResource.Name] = newResource;
                 continue;
             }
 
             if (mode == MergeMode.Replace ||
-                existing.GetType() != newResource.GetType())
-            {
+                existing.GetType() != newResource.GetType()) {
                 result[newResource.Name] = newResource;
                 continue;
             }
 
             DeepMerge(existing, newResource, mode);
-            
         }
 
         return result.Values.ToList();
     }
 
-    private static List<Resource> DeepCloneList(IEnumerable<Resource> resources)
-    {
-        var json = JsonSerializer.Serialize(resources, CloneJsonOptions);
-        return JsonSerializer.Deserialize<List<Resource>>(json, CloneJsonOptions) ?? new List<Resource>();
+    private static List<Resource> DeepCloneList(IEnumerable<Resource> resources) {
+        var json = JsonSerializer.Serialize(resources, _cloneJsonOptions);
+        return JsonSerializer.Deserialize<List<Resource>>(json, _cloneJsonOptions) ?? new List<Resource>();
     }
-    private static void DeepMerge(object target, object source, MergeMode mode)
-    {
-        var type = target.GetType();
 
-        foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
-        {
+    private static void DeepMerge(object target, object source, MergeMode mode) {
+        Type type = target.GetType();
+
+        foreach (PropertyInfo prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) {
             if (!prop.CanRead || !prop.CanWrite)
                 continue;
 
@@ -84,18 +75,16 @@ public static class ResourceCollectionMerger
                 continue;
 
             var targetValue = prop.GetValue(target);
-            var propType = prop.PropertyType;
+            Type propType = prop.PropertyType;
 
             // Simple types → overwrite
-            if (IsSimple(propType))
-            {
+            if (IsSimple(propType)) {
                 prop.SetValue(target, sourceValue);
                 continue;
             }
 
             // Dictionary
-            if (IsDictionary(propType))
-            {
+            if (IsDictionary(propType)) {
                 if (mode == MergeMode.Merge && IsDictionaryEmpty(sourceValue))
                     continue;
 
@@ -104,8 +93,7 @@ public static class ResourceCollectionMerger
             }
 
             // List / collection
-            if (IsEnumerable(propType))
-            {
+            if (IsEnumerable(propType)) {
                 if (mode == MergeMode.Merge && IsEnumerableEmpty(sourceValue))
                     continue;
 
@@ -115,18 +103,13 @@ public static class ResourceCollectionMerger
 
             // Complex object → recursive merge
             if (targetValue == null)
-            {
                 prop.SetValue(target, sourceValue);
-            }
             else
-            {
                 DeepMerge(targetValue, sourceValue, mode);
-            }
         }
     }
 
-    private static bool IsSimple(Type type)
-    {
+    private static bool IsSimple(Type type) {
         return type.IsPrimitive
                || type == typeof(string)
                || type == typeof(decimal)
@@ -135,56 +118,44 @@ public static class ResourceCollectionMerger
                || Nullable.GetUnderlyingType(type)?.IsPrimitive == true;
     }
 
-    private static bool IsDictionary(Type type)
-    {
+    private static bool IsDictionary(Type type) {
         return type.IsGenericType &&
                type.GetGenericTypeDefinition() == typeof(Dictionary<,>);
     }
 
-    private static bool IsEnumerable(Type type)
-    {
-        return typeof(System.Collections.IEnumerable).IsAssignableFrom(type)
+    private static bool IsEnumerable(Type type) {
+        return typeof(IEnumerable).IsAssignableFrom(type)
                && type != typeof(string)
                && !IsDictionary(type);
     }
-    private static bool IsEnumerableEmpty(object value)
-    {
-        var enumerable = (System.Collections.IEnumerable)value;
+
+    private static bool IsEnumerableEmpty(object value) {
+        var enumerable = (IEnumerable)value;
         return !enumerable.GetEnumerator().MoveNext();
     }
 
-    private static bool IsDictionaryEmpty(object value)
-    {
-        var dict = (System.Collections.IDictionary)value;
+    private static bool IsDictionaryEmpty(object value) {
+        var dict = (IDictionary)value;
         return dict.Count == 0;
     }
 
-    private static void MergeDictionaries(object? target, object source)
-    {
+    private static void MergeDictionaries(object? target, object source) {
         if (target == null) return;
 
-        var targetDict = (System.Collections.IDictionary)target;
-        var sourceDict = (System.Collections.IDictionary)source;
+        var targetDict = (IDictionary)target;
+        var sourceDict = (IDictionary)source;
 
-        foreach (var key in sourceDict.Keys)
-        {
-            targetDict[key] = sourceDict[key];
-        }
+        foreach (var key in sourceDict.Keys) targetDict[key] = sourceDict[key];
     }
 }
 
-internal static class ResourcePolymorphismResolver
-{
-    public static IJsonTypeInfoResolver Create()
-    {
+internal static class ResourcePolymorphismResolver {
+    public static IJsonTypeInfoResolver Create() {
         var resolver = new DefaultJsonTypeInfoResolver();
 
-        resolver.Modifiers.Add(typeInfo =>
-        {
-            if (typeInfo.Type == typeof(Resource))
-            {
-                typeInfo.PolymorphismOptions = new JsonPolymorphismOptions
-                {
+        resolver.Modifiers.Add(typeInfo => {
+            if (typeInfo.Type == typeof(Resource)) {
+                typeInfo.PolymorphismOptions = new JsonPolymorphismOptions {
                     TypeDiscriminatorPropertyName = "kind",
                     IgnoreUnrecognizedTypeDiscriminators = false,
                     UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization
@@ -224,4 +195,4 @@ internal static class ResourcePolymorphismResolver
 
         return resolver;
     }
-}
+}

+ 11 - 26
RackPeek.Domain/Persistence/ServiceRepository.cs

@@ -2,15 +2,10 @@ using RackPeek.Domain.Resources.Services;
 
 namespace RackPeek.Domain.Persistence;
 
-public class ServiceRepository(IResourceCollection resources) : IServiceRepository
-{
-    public Task<int> GetCountAsync()
-    {
-        return Task.FromResult(resources.ServiceResources.Count);
-    }
+public class ServiceRepository(IResourceCollection resources) : IServiceRepository {
+    public Task<int> GetCountAsync() => Task.FromResult(resources.ServiceResources.Count);
 
-    public Task<int> GetIpAddressCountAsync()
-    {
+    public Task<int> GetIpAddressCountAsync() {
         return Task.FromResult(resources.ServiceResources
             .Where(i => i.Network?.Ip != null)
             .Select(i => i.Network!.Ip)
@@ -18,26 +13,18 @@ public class ServiceRepository(IResourceCollection resources) : IServiceReposito
             .Count());
     }
 
-    public Task<IReadOnlyList<Service>> GetBySystemHostAsync(string systemHostName)
-    {
+    public Task<IReadOnlyList<Service>> GetBySystemHostAsync(string systemHostName) {
         var systemHostNameLower = systemHostName.ToLower().Trim();
         var results = resources.ServiceResources
             .Where(s => s.RunsOn.Select(p => p.ToLower().Equals(systemHostNameLower)).ToList().Count > 0).ToList();
         return Task.FromResult<IReadOnlyList<Service>>(results);
     }
 
-    public Task<IReadOnlyList<Service>> GetAllAsync()
-    {
-        return Task.FromResult(resources.ServiceResources);
-    }
+    public Task<IReadOnlyList<Service>> GetAllAsync() => Task.FromResult(resources.ServiceResources);
 
-    public Task<Service?> GetByNameAsync(string name)
-    {
-        return Task.FromResult(resources.GetByName(name) as Service);
-    }
+    public Task<Service?> GetByNameAsync(string name) => Task.FromResult(resources.GetByName(name) as Service);
 
-    public async Task AddAsync(Service service)
-    {
+    public async Task AddAsync(Service service) {
         if (resources.ServiceResources.Any(r =>
                 r.Name.Equals(service.Name, StringComparison.OrdinalIgnoreCase)))
             throw new InvalidOperationException(
@@ -46,9 +33,8 @@ public class ServiceRepository(IResourceCollection resources) : IServiceReposito
         await resources.AddAsync(service);
     }
 
-    public async Task UpdateAsync(Service service)
-    {
-        var existing = resources.ServiceResources
+    public async Task UpdateAsync(Service service) {
+        Service? existing = resources.ServiceResources
             .FirstOrDefault(r => r.Name.Equals(service.Name, StringComparison.OrdinalIgnoreCase));
 
         if (existing == null)
@@ -57,9 +43,8 @@ public class ServiceRepository(IResourceCollection resources) : IServiceReposito
         await resources.UpdateAsync(service);
     }
 
-    public async Task DeleteAsync(string name)
-    {
-        var existing = resources.ServiceResources
+    public async Task DeleteAsync(string name) {
+        Service? existing = resources.ServiceResources
             .FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
 
         if (existing == null)

+ 18 - 36
RackPeek.Domain/Persistence/SystemRepository.cs

@@ -2,23 +2,17 @@ using RackPeek.Domain.Resources.SystemResources;
 
 namespace RackPeek.Domain.Persistence;
 
-public class YamlSystemRepository(IResourceCollection resources) : ISystemRepository
-{
-    public Task<int> GetSystemCountAsync()
-    {
-        return Task.FromResult(resources.SystemResources.Count);
-    }
+public class YamlSystemRepository(IResourceCollection resources) : ISystemRepository {
+    public Task<int> GetSystemCountAsync() => Task.FromResult(resources.SystemResources.Count);
 
-    public Task<Dictionary<string, int>> GetSystemTypeCountAsync()
-    {
+    public Task<Dictionary<string, int>> GetSystemTypeCountAsync() {
         return Task.FromResult(resources.SystemResources
             .Where(s => !string.IsNullOrEmpty(s.Type))
             .GroupBy(h => h.Type!)
             .ToDictionary(k => k.Key, v => v.Count()));
     }
 
-    public Task<Dictionary<string, int>> GetSystemOsCountAsync()
-    {
+    public Task<Dictionary<string, int>> GetSystemOsCountAsync() {
         return Task.FromResult(resources.SystemResources
             .Where(s => !string.IsNullOrEmpty(s.Os))
             .GroupBy(h => h.Os!)
@@ -27,9 +21,8 @@ public class YamlSystemRepository(IResourceCollection resources) : ISystemReposi
 
     public Task<IReadOnlyList<SystemResource>> GetFilteredAsync(
         string? typeFilter,
-        string? osFilter)
-    {
-        var query = resources.SystemResources.AsQueryable();
+        string? osFilter) {
+        IQueryable<SystemResource> query = resources.SystemResources.AsQueryable();
 
         var type = Normalize(typeFilter);
         var os = Normalize(osFilter);
@@ -44,32 +37,23 @@ public class YamlSystemRepository(IResourceCollection resources) : ISystemReposi
         return Task.FromResult<IReadOnlyList<SystemResource>>(results);
     }
 
-    public Task<IReadOnlyList<SystemResource>> GetByPhysicalHostAsync(string physicalHostName)
-    {
+    public Task<IReadOnlyList<SystemResource>> GetByPhysicalHostAsync(string physicalHostName) {
         var physicalHostNameLower = physicalHostName.ToLower().Trim();
         var results = resources.SystemResources
-            .Where(s => s.RunsOn.Select(sys => sys.ToLower().Equals(physicalHostNameLower)).ToList().Count > 0).ToList();
+            .Where(s => s.RunsOn.Select(sys => sys.ToLower().Equals(physicalHostNameLower)).ToList().Count > 0)
+            .ToList();
         return Task.FromResult<IReadOnlyList<SystemResource>>(results);
     }
 
-    public Task<IReadOnlyList<SystemResource>> GetAllAsync()
-    {
-        return Task.FromResult(resources.SystemResources);
-    }
+    public Task<IReadOnlyList<SystemResource>> GetAllAsync() => Task.FromResult(resources.SystemResources);
 
-    private static string? Normalize(string? value)
-    {
-        return string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLower();
-    }
+    private static string? Normalize(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLower();
 
 
-    public Task<SystemResource?> GetByNameAsync(string name)
-    {
-        return Task.FromResult(resources.GetByName(name) as SystemResource);
-    }
+    public Task<SystemResource?> GetByNameAsync(string name) =>
+        Task.FromResult(resources.GetByName(name) as SystemResource);
 
-    public async Task AddAsync(SystemResource systemResource)
-    {
+    public async Task AddAsync(SystemResource systemResource) {
         if (resources.SystemResources.Any(r =>
                 r.Name.Equals(systemResource.Name, StringComparison.OrdinalIgnoreCase)))
             throw new InvalidOperationException(
@@ -78,9 +62,8 @@ public class YamlSystemRepository(IResourceCollection resources) : ISystemReposi
         await resources.AddAsync(systemResource);
     }
 
-    public async Task UpdateAsync(SystemResource systemResource)
-    {
-        var existing = resources.SystemResources
+    public async Task UpdateAsync(SystemResource systemResource) {
+        SystemResource? existing = resources.SystemResources
             .FirstOrDefault(r => r.Name.Equals(systemResource.Name, StringComparison.OrdinalIgnoreCase));
 
         if (existing == null)
@@ -89,9 +72,8 @@ public class YamlSystemRepository(IResourceCollection resources) : ISystemReposi
         await resources.UpdateAsync(systemResource);
     }
 
-    public async Task DeleteAsync(string name)
-    {
-        var existing = resources.SystemResources
+    public async Task DeleteAsync(string name) {
+        SystemResource? existing = resources.SystemResources
             .FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
 
         if (existing == null)

+ 15 - 22
RackPeek.Domain/Persistence/Yaml/Converters.cs

@@ -6,45 +6,40 @@ using YamlDotNet.Serialization;
 
 namespace RackPeek.Domain.Persistence.Yaml;
 
-public static class StorageSizeParser
-{
-    private static readonly Regex SizeRegex = new(@"^\s*(\d+(?:\.\d+)?)\s*(gb|tb)?\s*$",
+public static class StorageSizeParser {
+    private static readonly Regex _sizeRegex = new(@"^\s*(\d+(?:\.\d+)?)\s*(gb|tb)?\s*$",
         RegexOptions.IgnoreCase | RegexOptions.Compiled);
 
-    public static double ParseToGbDouble(string input)
-    {
-        var match = SizeRegex.Match(input);
+    public static double ParseToGbDouble(string input) {
+        Match match = _sizeRegex.Match(input);
         if (!match.Success) throw new FormatException($"Invalid storage size: '{input}'");
         var value = double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
         var unit = match.Groups[2].Value.ToLowerInvariant();
-        return unit switch
-        {
-            "tb" => value * 1024, "gb" or "" => value, _ => throw new FormatException($"Unknown unit in '{input}'")
+        return unit switch {
+            "tb" => value * 1024,
+            "gb" or "" => value,
+            _ => throw new FormatException($"Unknown unit in '{input}'")
         };
     }
 }
 
-public class StorageSizeYamlConverter : IYamlTypeConverter
-{
-    public bool Accepts(Type type)
-    {
+public class StorageSizeYamlConverter : IYamlTypeConverter {
+    public bool Accepts(Type type) {
         return type == typeof(int) ||
                type == typeof(int?) ||
                type == typeof(double) ||
                type == typeof(double?);
     }
 
-    public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
-    {
-        var scalar = parser.Consume<Scalar>();
+    public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) {
+        Scalar scalar = parser.Consume<Scalar>();
         var value = scalar.Value;
 
         if (string.IsNullOrWhiteSpace(value))
             return null;
 
         // If it's already a number, parse directly
-        if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var numericDouble))
-        {
+        if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var numericDouble)) {
             if (type == typeof(double) || type == typeof(double?))
                 return numericDouble;
 
@@ -61,8 +56,6 @@ public class StorageSizeYamlConverter : IYamlTypeConverter
         return (int)Math.Round(gb);
     }
 
-    public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer)
-    {
+    public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) =>
         emitter.Emit(new Scalar(value?.ToString() ?? string.Empty));
-    }
-}
+}

+ 6 - 17
RackPeek.Domain/Persistence/Yaml/ITextFileStore.cs

@@ -1,26 +1,15 @@
 namespace RackPeek.Domain.Persistence.Yaml;
 
-public interface ITextFileStore
-{
+public interface ITextFileStore {
     Task<bool> ExistsAsync(string path);
     Task<string> ReadAllTextAsync(string path);
     Task WriteAllTextAsync(string path, string contents);
 }
 
-public sealed class PhysicalTextFileStore : ITextFileStore
-{
-    public Task<bool> ExistsAsync(string path)
-    {
-        return Task.FromResult(File.Exists(path));
-    }
+public sealed class PhysicalTextFileStore : ITextFileStore {
+    public Task<bool> ExistsAsync(string path) => Task.FromResult(File.Exists(path));
 
-    public Task<string> ReadAllTextAsync(string path)
-    {
-        return File.ReadAllTextAsync(path);
-    }
+    public Task<string> ReadAllTextAsync(string path) => File.ReadAllTextAsync(path);
 
-    public Task WriteAllTextAsync(string path, string contents)
-    {
-        return File.WriteAllTextAsync(path, contents);
-    }
-}
+    public Task WriteAllTextAsync(string path, string contents) => File.WriteAllTextAsync(path, contents);
+}

+ 7 - 14
RackPeek.Domain/Persistence/Yaml/NotesStringYamlConverter.cs

@@ -4,23 +4,16 @@ using YamlDotNet.Serialization;
 
 namespace RackPeek.Domain.Persistence.Yaml;
 
-public sealed class NotesStringYamlConverter : IYamlTypeConverter
-{
-    public bool Accepts(Type type)
-    {
-        return type == typeof(string);
-    }
+public sealed class NotesStringYamlConverter : IYamlTypeConverter {
+    public bool Accepts(Type type) => type == typeof(string);
 
-    public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
-    {
-        var scalar = parser.Consume<Scalar>();
+    public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) {
+        Scalar scalar = parser.Consume<Scalar>();
         return scalar.Value;
     }
 
-    public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer)
-    {
-        if (value is null)
-        {
+    public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) {
+        if (value is null) {
             emitter.Emit(new Scalar(
                 AnchorName.Empty,
                 TagName.Empty,
@@ -45,4 +38,4 @@ public sealed class NotesStringYamlConverter : IYamlTypeConverter
         else
             emitter.Emit(new Scalar(s));
     }
-}
+}

+ 20 - 24
RackPeek.Domain/Persistence/Yaml/RackPeekConfigMigrationDeserializer.cs

@@ -16,25 +16,25 @@ using YamlDotNet.Serialization.NamingConventions;
 
 namespace RackPeek.Domain.Persistence.Yaml;
 
-public class RackPeekConfigMigrationDeserializer : YamlMigrationDeserializer<YamlRoot>
-{
+public class RackPeekConfigMigrationDeserializer : YamlMigrationDeserializer<YamlRoot> {
     // List migration functions here
-    public static readonly IReadOnlyList<Func<IServiceProvider, Dictionary<object, object>, ValueTask>> ListOfMigrations = new List<Func<IServiceProvider, Dictionary<object,object>, ValueTask>>{
-        EnsureSchemaVersionExists,
-        ConvertScalarRunsOnToList,
-    };
+    public static readonly IReadOnlyList<Func<IServiceProvider, Dictionary<object, object>, ValueTask>>
+        ListOfMigrations = new List<Func<IServiceProvider, Dictionary<object, object>, ValueTask>>
+        {
+            EnsureSchemaVersionExists,
+            ConvertScalarRunsOnToList
+        };
 
     public RackPeekConfigMigrationDeserializer(IServiceProvider serviceProvider,
         ILogger<YamlMigrationDeserializer<YamlRoot>> logger) :
-        base(serviceProvider, logger, 
+        base(serviceProvider, logger,
             ListOfMigrations,
             "version",
             new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance)
                 .WithCaseInsensitivePropertyMatching()
                 .WithTypeConverter(new StorageSizeYamlConverter())
                 .WithTypeConverter(new NotesStringYamlConverter())
-                .WithTypeDiscriminatingNodeDeserializer(options =>
-                {
+                .WithTypeDiscriminatingNodeDeserializer(options => {
                     options.AddKeyValueTypeDiscriminator<Resource>("kind", new Dictionary<string, Type>
                     {
                         { Server.KindLabel, typeof(Server) },
@@ -48,7 +48,7 @@ public class RackPeekConfigMigrationDeserializer : YamlMigrationDeserializer<Yam
                         { SystemResource.KindLabel, typeof(SystemResource) },
                         { Service.KindLabel, typeof(Service) }
                     });
-                }), 
+                }),
             new SerializerBuilder()
                 .WithNamingConvention(CamelCaseNamingConvention.Instance)
                 .WithTypeConverter(new StorageSizeYamlConverter())
@@ -56,24 +56,21 @@ public class RackPeekConfigMigrationDeserializer : YamlMigrationDeserializer<Yam
                 .ConfigureDefaultValuesHandling(
                     DefaultValuesHandling.OmitNull |
                     DefaultValuesHandling.OmitEmptyCollections
-                )) {}
+                )) {
+    }
 
     #region Migrations
 
     // Define migration functions here
-    public static ValueTask EnsureSchemaVersionExists(IServiceProvider serviceProvider, Dictionary<object, object> obj)
-    {
-        if (!obj.ContainsKey("version"))
-        {
-            obj["version"] = 0;
-        }
-        
+    public static ValueTask EnsureSchemaVersionExists(IServiceProvider serviceProvider, Dictionary<object, object> obj) {
+        if (!obj.ContainsKey("version")) obj["version"] = 0;
+
         return ValueTask.CompletedTask;
     }
+
     public static ValueTask ConvertScalarRunsOnToList(
         IServiceProvider serviceProvider,
-        Dictionary<object, object> obj)
-    {
+        Dictionary<object, object> obj) {
         const string key = "runsOn";
 
         if (!obj.TryGetValue("resources", out var resourceListObj))
@@ -82,16 +79,14 @@ public class RackPeekConfigMigrationDeserializer : YamlMigrationDeserializer<Yam
         if (resourceListObj is not List<object> resources)
             return ValueTask.CompletedTask;
 
-        foreach (var resourceObj in resources)
-        {
+        foreach (var resourceObj in resources) {
             if (resourceObj is not Dictionary<object, object> resourceDict)
                 continue;
 
             if (!resourceDict.TryGetValue(key, out var runsOn))
                 continue;
 
-            switch (runsOn)
-            {
+            switch (runsOn) {
                 case string single:
                     resourceDict[key] = new List<string> { single };
                     break;
@@ -114,5 +109,6 @@ public class RackPeekConfigMigrationDeserializer : YamlMigrationDeserializer<Yam
 
         return ValueTask.CompletedTask;
     }
+
     #endregion
 }

+ 10 - 20
RackPeek.Domain/Persistence/Yaml/ResourceYamlMigrationService.cs

@@ -1,43 +1,34 @@
 namespace RackPeek.Domain.Persistence.Yaml;
 
-using RackPeek.Domain.Resources;
-using YamlDotNet.Core;
-
-public interface IResourceYamlMigrationService
-{
+public interface IResourceYamlMigrationService {
     Task<YamlRoot> DeserializeAsync(
         string yaml,
         Func<string, Task>? preMigrationAction = null,
         Func<YamlRoot, Task>? postMigrationAction = null);
 }
 
-public sealed class ResourceYamlMigrationService( 
+public sealed class ResourceYamlMigrationService(
     RackPeekConfigMigrationDeserializer deserializer)
-    : IResourceYamlMigrationService
-{
-    private static readonly int CurrentSchemaVersion =
+    : IResourceYamlMigrationService {
+    private static readonly int _currentSchemaVersion =
         RackPeekConfigMigrationDeserializer.ListOfMigrations.Count;
 
     public async Task<YamlRoot> DeserializeAsync(
         string yaml,
         Func<string, Task>? preMigrationAction = null,
-        Func<YamlRoot, Task>? postMigrationAction = null)
-    {
+        Func<YamlRoot, Task>? postMigrationAction = null) {
         if (string.IsNullOrWhiteSpace(yaml))
             return new YamlRoot();
 
         var version = deserializer.GetSchemaVersion(yaml);
 
-        if (version > CurrentSchemaVersion)
-        {
+        if (version > _currentSchemaVersion)
             throw new InvalidOperationException(
-                $"Config schema version {version} is newer than this application supports ({CurrentSchemaVersion}).");
-        }
+                $"Config schema version {version} is newer than this application supports ({_currentSchemaVersion}).");
 
         YamlRoot? root;
 
-        if (version < CurrentSchemaVersion)
-        {
+        if (version < _currentSchemaVersion) {
             if (preMigrationAction != null)
                 await preMigrationAction(yaml);
 
@@ -46,11 +37,10 @@ public sealed class ResourceYamlMigrationService(
             if (postMigrationAction != null)
                 await postMigrationAction(root);
         }
-        else
-        {
+        else {
             root = await deserializer.Deserialize(yaml);
         }
 
         return root ?? new YamlRoot();
     }
-}
+}

+ 128 - 170
RackPeek.Domain/Persistence/Yaml/YamlResourceCollection.cs

@@ -1,4 +1,6 @@
+using System.Collections.ObjectModel;
 using System.Collections.Specialized;
+using System.Diagnostics;
 using RackPeek.Domain.Resources;
 using RackPeek.Domain.Resources.AccessPoints;
 using RackPeek.Domain.Resources.Desktops;
@@ -8,18 +10,15 @@ using RackPeek.Domain.Resources.Laptops;
 using RackPeek.Domain.Resources.Routers;
 using RackPeek.Domain.Resources.Servers;
 using RackPeek.Domain.Resources.Services;
-using RackPeek.Domain.Resources.Switches;
 using RackPeek.Domain.Resources.SystemResources;
 using RackPeek.Domain.Resources.UpsUnits;
-using YamlDotNet.Core;
 using YamlDotNet.Serialization;
 using YamlDotNet.Serialization.NamingConventions;
+using Switch = RackPeek.Domain.Resources.Switches.Switch;
 
 namespace RackPeek.Domain.Persistence.Yaml;
 
-
-public class ResourceCollection
-{
+public class ResourceCollection {
     public readonly SemaphoreSlim FileLock = new(1, 1);
     public List<Resource> Resources { get; } = new();
 }
@@ -29,26 +28,22 @@ public sealed class YamlResourceCollection(
     ITextFileStore fileStore,
     ResourceCollection resourceCollection,
     IResourceYamlMigrationService migrationService)
-    : IResourceCollection
-{
+    : IResourceCollection {
     // Bump this when your YAML schema changes, and add a migration step below.
-    private static readonly int CurrentSchemaVersion = RackPeekConfigMigrationDeserializer.ListOfMigrations.Count;
+    private static readonly int _currentSchemaVersion = RackPeekConfigMigrationDeserializer.ListOfMigrations.Count;
 
-    public Task<bool> Exists(string name)
-    {
+    public Task<bool> Exists(string name) {
         return Task.FromResult(resourceCollection.Resources.Exists(r =>
             r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
     }
 
-    public Task<string?> GetKind(string? name)
-    {
+    public Task<string?> GetKind(string? name) {
         return Task.FromResult(resourceCollection.Resources.FirstOrDefault(r =>
             r.Name.Equals(name, StringComparison.OrdinalIgnoreCase))?.Kind);
-        
     }
-    public Task<IReadOnlyList<(Resource, string)>> GetByLabelAsync(string name)
-    {
-        var result = resourceCollection.Resources
+
+    public Task<IReadOnlyList<(Resource, string)>> GetByLabelAsync(string name) {
+        ReadOnlyCollection<(Resource r, string)> result = resourceCollection.Resources
             .Where(r => r.Labels != null && r.Labels.TryGetValue(name, out _))
             .Select(r => (r, r.Labels![name]))
             .ToList()
@@ -56,8 +51,8 @@ public sealed class YamlResourceCollection(
 
         return Task.FromResult<IReadOnlyList<(Resource, string)>>(result);
     }
-    public Task<Dictionary<string, int>> GetLabelsAsync()
-    {
+
+    public Task<Dictionary<string, int>> GetLabelsAsync() {
         var result = resourceCollection.Resources
             .SelectMany(r => r.Labels ?? Enumerable.Empty<KeyValuePair<string, string>>())
             .Where(kvp => !string.IsNullOrWhiteSpace(kvp.Key))
@@ -66,11 +61,11 @@ public sealed class YamlResourceCollection(
 
         return Task.FromResult(result);
     }
-    public Task<IReadOnlyList<(Resource, string)>> GetResourceIpsAsync()
-    {
+
+    public Task<IReadOnlyList<(Resource, string)>> GetResourceIpsAsync() {
         var result = new List<(Resource, string)>();
 
-        var allResources = resourceCollection.Resources;
+        List<Resource> allResources = resourceCollection.Resources;
 
         // Build fast lookup for systems
         var systemsByName = allResources
@@ -80,88 +75,27 @@ public sealed class YamlResourceCollection(
         // Cache resolved system IPs (prevents repeated recursion)
         var resolvedSystemIps = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
 
-        foreach (var resource in allResources)
-        {
-            switch (resource)
-            {
-                case SystemResource system:
-                {
-                    var ip = ResolveSystemIp(system, systemsByName, resolvedSystemIps);
-                    if (!string.IsNullOrWhiteSpace(ip))
-                        result.Add((system, ip));
-                    break;
-                }
-
-                case Service service:
-                {
-                    var ip = ResolveServiceIp(service, systemsByName, resolvedSystemIps);
-                    if (!string.IsNullOrWhiteSpace(ip))
-                        result.Add((service, ip));
-                    break;
-                }
+        foreach (Resource resource in allResources)
+            switch (resource) {
+                case SystemResource system: {
+                        var ip = ResolveSystemIp(system, systemsByName, resolvedSystemIps);
+                        if (!string.IsNullOrWhiteSpace(ip))
+                            result.Add((system, ip));
+                        break;
+                    }
+
+                case Service service: {
+                        var ip = ResolveServiceIp(service, systemsByName, resolvedSystemIps);
+                        if (!string.IsNullOrWhiteSpace(ip))
+                            result.Add((service, ip));
+                        break;
+                    }
             }
-        }
 
         return Task.FromResult((IReadOnlyList<(Resource, string)>)result);
     }
-    private string? ResolveSystemIp(
-        SystemResource system,
-        Dictionary<string, SystemResource> systemsByName,
-        Dictionary<string, string?> cache)
-    {
-        // Return cached result if already resolved
-        if (cache.TryGetValue(system.Name, out var cached))
-            return cached;
-
-        // Direct IP wins
-        if (!string.IsNullOrWhiteSpace(system.Ip))
-        {
-            cache[system.Name] = system.Ip;
-            return system.Ip;
-        }
-
-        // Must have exactly one parent
-        if (system.RunsOn?.Count != 1)
-        {
-            cache[system.Name] = null;
-            return null;
-        }
-
-        var parentName = system.RunsOn.First();
-
-        if (!systemsByName.TryGetValue(parentName, out var parent))
-        {
-            cache[system.Name] = null;
-            return null;
-        }
-
-        var resolved = ResolveSystemIp(parent, systemsByName, cache);
-        cache[system.Name] = resolved;
-
-        return resolved;
-    }
-    private string? ResolveServiceIp(
-        Service service,
-        Dictionary<string, SystemResource> systemsByName,
-        Dictionary<string, string?> cache)
-    {
-        // Direct IP wins
-        if (!string.IsNullOrWhiteSpace(service.Network?.Ip))
-            return service.Network!.Ip;
-
-        // Must have exactly one parent
-        if (service.RunsOn?.Count != 1)
-            return null;
-
-        var parentName = service.RunsOn.First();
 
-        if (!systemsByName.TryGetValue(parentName, out var parent))
-            return null;
-
-        return ResolveSystemIp(parent, systemsByName, cache);
-    }
-    public Task<Dictionary<string, int>> GetTagsAsync()
-    {
+    public Task<Dictionary<string, int>> GetTagsAsync() {
         var result = resourceCollection.Resources
             .SelectMany(r => r.Tags) // flatten all tag arrays
             .Where(t => !string.IsNullOrWhiteSpace(t))
@@ -171,13 +105,10 @@ public sealed class YamlResourceCollection(
         return Task.FromResult(result);
     }
 
-    public Task<IReadOnlyList<T>> GetAllOfTypeAsync<T>()
-    {
-        return Task.FromResult<IReadOnlyList<T>>(resourceCollection.Resources.OfType<T>().ToList());
-    }
-    
-    public Task<IReadOnlyList<Resource>> GetDependantsAsync(string name)
-    {
+    public Task<IReadOnlyList<T>> GetAllOfTypeAsync<T>() =>
+        Task.FromResult<IReadOnlyList<T>>(resourceCollection.Resources.OfType<T>().ToList());
+
+    public Task<IReadOnlyList<Resource>> GetDependantsAsync(string name) {
         var result = resourceCollection.Resources
             .Where(r => r.RunsOn.Any(p => p.Equals(name, StringComparison.OrdinalIgnoreCase)))
             .ToList();
@@ -185,18 +116,16 @@ public sealed class YamlResourceCollection(
         return Task.FromResult<IReadOnlyList<Resource>>(result);
     }
 
-    public async Task Merge(string incomingYaml, MergeMode mode)
-    {
+    public async Task Merge(string incomingYaml, MergeMode mode) {
         if (string.IsNullOrWhiteSpace(incomingYaml))
             return;
 
         await resourceCollection.FileLock.WaitAsync();
-        try
-        {
-            var incomingRoot = await migrationService.DeserializeAsync(incomingYaml);
+        try {
+            YamlRoot incomingRoot = await migrationService.DeserializeAsync(incomingYaml);
 
-            var incomingResources = incomingRoot.Resources ?? new List<Resource>();
-            var merged = ResourceCollectionMerger.Merge(
+            List<Resource> incomingResources = incomingRoot.Resources ?? new List<Resource>();
+            List<Resource> merged = ResourceCollectionMerger.Merge(
                 resourceCollection.Resources,
                 incomingResources,
                 mode);
@@ -204,22 +133,19 @@ public sealed class YamlResourceCollection(
             resourceCollection.Resources.Clear();
             resourceCollection.Resources.AddRange(merged);
 
-            var rootToSave = new YamlRoot
-            {
+            var rootToSave = new YamlRoot {
                 Version = RackPeekConfigMigrationDeserializer.ListOfMigrations.Count,
                 Resources = resourceCollection.Resources
             };
 
             await SaveRootAsync(rootToSave);
         }
-        finally
-        {
+        finally {
             resourceCollection.FileLock.Release();
         }
     }
 
-    public Task<IReadOnlyList<Resource>> GetByTagAsync(string name)
-    {
+    public Task<IReadOnlyList<Resource>> GetByTagAsync(string name) {
         return Task.FromResult<IReadOnlyList<Resource>>(
             resourceCollection.Resources
                 .Where(r => r.Tags.Contains(name))
@@ -236,30 +162,26 @@ public sealed class YamlResourceCollection(
     public IReadOnlyList<Service> ServiceResources =>
         resourceCollection.Resources.OfType<Service>().ToList();
 
-    public Task<Resource?> GetByNameAsync(string name)
-    {
+    public Task<Resource?> GetByNameAsync(string name) {
         return Task.FromResult(resourceCollection.Resources.FirstOrDefault(r =>
             r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
     }
 
-    public Task<T?> GetByNameAsync<T>(string name) where T : Resource
-    {
-        var resource =
+    public Task<T?> GetByNameAsync<T>(string name) where T : Resource {
+        Resource? resource =
             resourceCollection.Resources.FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
         return Task.FromResult(resource as T);
     }
 
-    public Resource? GetByName(string name)
-    {
+    public Resource? GetByName(string name) {
         return resourceCollection.Resources.FirstOrDefault(r =>
             r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
     }
 
-    public async Task LoadAsync()
-    {
+    public async Task LoadAsync() {
         var yaml = await fileStore.ReadAllTextAsync(filePath);
 
-        var root = await migrationService.DeserializeAsync(
+        YamlRoot root = await migrationService.DeserializeAsync(
             yaml,
             async originalYaml => await BackupOriginalAsync(originalYaml),
             async migratedRoot => await SaveRootAsync(migratedRoot)
@@ -270,11 +192,9 @@ public sealed class YamlResourceCollection(
         if (root.Resources != null)
             resourceCollection.Resources.AddRange(root.Resources);
     }
-    
-    public Task AddAsync(Resource resource)
-    {
-        return UpdateWithLockAsync(list =>
-        {
+
+    public Task AddAsync(Resource resource) {
+        return UpdateWithLockAsync(list => {
             if (list.Any(r => r.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase)))
                 throw new InvalidOperationException($"'{resource.Name}' already exists.");
 
@@ -283,10 +203,8 @@ public sealed class YamlResourceCollection(
         });
     }
 
-    public Task UpdateAsync(Resource resource)
-    {
-        return UpdateWithLockAsync(list =>
-        {
+    public Task UpdateAsync(Resource resource) {
+        return UpdateWithLockAsync(list => {
             var index = list.FindIndex(r => r.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase));
             if (index == -1) throw new InvalidOperationException("Not found.");
 
@@ -295,30 +213,78 @@ public sealed class YamlResourceCollection(
         });
     }
 
-    public Task DeleteAsync(string name)
-    {
+    public Task DeleteAsync(string name) {
         return UpdateWithLockAsync(list =>
             list.RemoveAll(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
     }
 
-    private async Task UpdateWithLockAsync(Action<List<Resource>> action)
-    {
+    private string? ResolveSystemIp(
+        SystemResource system,
+        Dictionary<string, SystemResource> systemsByName,
+        Dictionary<string, string?> cache) {
+        // Return cached result if already resolved
+        if (cache.TryGetValue(system.Name, out var cached))
+            return cached;
+
+        // Direct IP wins
+        if (!string.IsNullOrWhiteSpace(system.Ip)) {
+            cache[system.Name] = system.Ip;
+            return system.Ip;
+        }
+
+        // Must have exactly one parent
+        if (system.RunsOn?.Count != 1) {
+            cache[system.Name] = null;
+            return null;
+        }
+
+        var parentName = system.RunsOn.First();
+
+        if (!systemsByName.TryGetValue(parentName, out SystemResource? parent)) {
+            cache[system.Name] = null;
+            return null;
+        }
+
+        var resolved = ResolveSystemIp(parent, systemsByName, cache);
+        cache[system.Name] = resolved;
+
+        return resolved;
+    }
+
+    private string? ResolveServiceIp(
+        Service service,
+        Dictionary<string, SystemResource> systemsByName,
+        Dictionary<string, string?> cache) {
+        // Direct IP wins
+        if (!string.IsNullOrWhiteSpace(service.Network?.Ip))
+            return service.Network!.Ip;
+
+        // Must have exactly one parent
+        if (service.RunsOn?.Count != 1)
+            return null;
+
+        var parentName = service.RunsOn.First();
+
+        if (!systemsByName.TryGetValue(parentName, out SystemResource? parent))
+            return null;
+
+        return ResolveSystemIp(parent, systemsByName, cache);
+    }
+
+    private async Task UpdateWithLockAsync(Action<List<Resource>> action) {
         await resourceCollection.FileLock.WaitAsync();
-        try
-        {
+        try {
             action(resourceCollection.Resources);
 
             // Always write current schema version when app writes the file.
-            var root = new YamlRoot
-            {
-                Version = CurrentSchemaVersion,
+            var root = new YamlRoot {
+                Version = _currentSchemaVersion,
                 Resources = resourceCollection.Resources
             };
 
             await SaveRootAsync(root);
         }
-        finally
-        {
+        finally {
             resourceCollection.FileLock.Release();
         }
     }
@@ -327,16 +293,14 @@ public sealed class YamlResourceCollection(
     // Versioning + migration
     // ----------------------------
 
-    private async Task BackupOriginalAsync(string originalYaml)
-    {
+    private async Task BackupOriginalAsync(string originalYaml) {
         // Timestamped backup for safe rollback
         var backupPath = $"{filePath}.bak.{DateTime.UtcNow:yyyyMMddHHmmss}";
         await fileStore.WriteAllTextAsync(backupPath, originalYaml);
     }
-    
-    private async Task SaveRootAsync(YamlRoot? root)
-    {
-        var serializer = new SerializerBuilder()
+
+    private async Task SaveRootAsync(YamlRoot? root) {
+        ISerializer serializer = new SerializerBuilder()
             .WithNamingConvention(CamelCaseNamingConvention.Instance)
             .WithTypeConverter(new StorageSizeYamlConverter())
             .WithTypeConverter(new NotesStringYamlConverter())
@@ -347,8 +311,8 @@ public sealed class YamlResourceCollection(
             .Build();
 
         // Preserve ordering: version first, then resources
-        var payload = new OrderedDictionary
-        {
+        Debug.Assert(root != null, nameof(root) + " != null");
+        var payload = new OrderedDictionary {
             ["version"] = root.Version,
             ["resources"] = (root.Resources ?? new List<Resource>()).Select(SerializeResource).ToList()
         };
@@ -356,10 +320,8 @@ public sealed class YamlResourceCollection(
         await fileStore.WriteAllTextAsync(filePath, serializer.Serialize(payload));
     }
 
-    private string GetKind(Resource resource)
-    {
-        return resource switch
-        {
+    private string GetKind(Resource resource) {
+        return resource switch {
             Server => "Server",
             Switch => "Switch",
             Firewall => "Firewall",
@@ -374,14 +336,12 @@ public sealed class YamlResourceCollection(
         };
     }
 
-    private OrderedDictionary SerializeResource(Resource resource)
-    {
-        var map = new OrderedDictionary
-        {
+    private OrderedDictionary SerializeResource(Resource resource) {
+        var map = new OrderedDictionary {
             ["kind"] = GetKind(resource)
         };
 
-        var serializer = new SerializerBuilder()
+        ISerializer serializer = new SerializerBuilder()
             .WithNamingConvention(CamelCaseNamingConvention.Instance)
             .WithTypeConverter(new NotesStringYamlConverter())
             .ConfigureDefaultValuesHandling(
@@ -392,21 +352,19 @@ public sealed class YamlResourceCollection(
 
         var yaml = serializer.Serialize(resource);
 
-        var props = new DeserializerBuilder()
+        Dictionary<string, object?> props = new DeserializerBuilder()
             .Build()
             .Deserialize<Dictionary<string, object?>>(yaml);
 
-        foreach (var (key, value) in props)
+        foreach ((var key, var value) in props)
             if (!string.Equals(key, "kind", StringComparison.OrdinalIgnoreCase))
                 map[key] = value;
 
         return map;
     }
-
 }
 
-public class YamlRoot
-{
+public class YamlRoot {
     public int Version { get; set; }
     public List<Resource>? Resources { get; set; }
 }

+ 4 - 4
RackPeek.Domain/RackPeek.Domain.csproj

@@ -7,10 +7,10 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <PackageReference Include="DocMigrator.Yaml" Version="10.0.3" />
-        <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
-        <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
-        <PackageReference Include="YamlDotNet" Version="16.3.0" />
+        <PackageReference Include="DocMigrator.Yaml" Version="10.0.3"/>
+        <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3"/>
+        <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3"/>
+        <PackageReference Include="YamlDotNet" Version="16.3.0"/>
     </ItemGroup>
 
 </Project>

+ 2 - 3
RackPeek.Domain/Resources/AccessPoints/AccessPoint.cs

@@ -1,8 +1,7 @@
 namespace RackPeek.Domain.Resources.AccessPoints;
 
-public class AccessPoint : Hardware.Hardware
-{
+public class AccessPoint : Hardware.Hardware {
     public const string KindLabel = "AccessPoint";
     public string? Model { get; set; }
     public double? Speed { get; set; }
-}
+}

+ 4 - 6
RackPeek.Domain/Resources/AccessPoints/AccessPointHardwareReport.cs

@@ -12,11 +12,9 @@ public record AccessPointHardwareRow(
     double SpeedGb
 );
 
-public class AccessPointHardwareReportUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<AccessPointHardwareReport> ExecuteAsync()
-    {
-        var aps = await repository.GetAllOfTypeAsync<AccessPoint>();
+public class AccessPointHardwareReportUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<AccessPointHardwareReport> ExecuteAsync() {
+        IReadOnlyList<AccessPoint> aps = await repository.GetAllOfTypeAsync<AccessPoint>();
         var rows = aps.Select(ap => new AccessPointHardwareRow(
             ap.Name,
             ap.Model ?? "Unknown",
@@ -25,4 +23,4 @@ public class AccessPointHardwareReportUseCase(IResourceCollection repository) :
 
         return new AccessPointHardwareReport(rows);
     }
-}
+}

+ 5 - 9
RackPeek.Domain/Resources/AccessPoints/UpdateAccessPointUseCase.cs

@@ -3,15 +3,13 @@ using RackPeek.Domain.Persistence;
 
 namespace RackPeek.Domain.Resources.AccessPoints;
 
-public class UpdateAccessPointUseCase(IResourceCollection repository) : IUseCase
-{
+public class UpdateAccessPointUseCase(IResourceCollection repository) : IUseCase {
     public async Task ExecuteAsync(
         string name,
         string? model = null,
         double? speed = null,
         string? notes = null
-    )
-    {
+    ) {
         // ToDo validate / normalize all inputs
 
         name = Normalize.HardwareName(name);
@@ -20,14 +18,12 @@ public class UpdateAccessPointUseCase(IResourceCollection repository) : IUseCase
         if (ap == null)
             throw new NotFoundException($"Access point '{name}' not found.");
 
-        if (!string.IsNullOrWhiteSpace(model))
-        {
+        if (!string.IsNullOrWhiteSpace(model)) {
             ThrowIfInvalid.AccessPointModelName(model);
             ap.Model = model;
         }
 
-        if (speed.HasValue)
-        {
+        if (speed.HasValue) {
             ThrowIfInvalid.NetworkSpeed(speed.Value);
             ap.Speed = speed.Value;
         }
@@ -36,4 +32,4 @@ public class UpdateAccessPointUseCase(IResourceCollection repository) : IUseCase
 
         await repository.UpdateAsync(ap);
     }
-}
+}

+ 3 - 5
RackPeek.Domain/Resources/Desktops/DescribeDesktopUseCase.cs

@@ -14,10 +14,8 @@ public record DesktopDescription(
     Dictionary<string, string> Labels
 );
 
-public class DescribeDesktopUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<DesktopDescription> ExecuteAsync(string name)
-    {
+public class DescribeDesktopUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<DesktopDescription> ExecuteAsync(string name) {
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
@@ -40,4 +38,4 @@ public class DescribeDesktopUseCase(IResourceCollection repository) : IUseCase
             desktop.Labels
         );
     }
-}
+}

+ 3 - 4
RackPeek.Domain/Resources/Desktops/Desktop.cs

@@ -3,13 +3,12 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Desktops;
 
-public class Desktop : Hardware.Hardware, ICpuResource, IDriveResource, IGpuResource, INicResource
-{
+public class Desktop : Hardware.Hardware, ICpuResource, IDriveResource, IGpuResource, INicResource {
     public const string KindLabel = "Desktop";
     public Ram? Ram { get; set; }
-    public string Model { get; set; }
+    public string? Model { get; set; }
     public List<Cpu>? Cpus { get; set; }
     public List<Drive>? Drives { get; set; }
     public List<Gpu>? Gpus { get; set; }
     public List<Nic>? Nics { get; set; }
-}
+}

+ 6 - 10
RackPeek.Domain/Resources/Desktops/DesktopHardwareReport.cs

@@ -19,14 +19,11 @@ public record DesktopHardwareRow(
     string GpuSummary
 );
 
-public class DesktopHardwareReportUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<DesktopHardwareReport> ExecuteAsync()
-    {
-        var desktops = await repository.GetAllOfTypeAsync<Desktop>();
+public class DesktopHardwareReportUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<DesktopHardwareReport> ExecuteAsync() {
+        IReadOnlyList<Desktop> desktops = await repository.GetAllOfTypeAsync<Desktop>();
 
-        var rows = desktops.Select(desktop =>
-        {
+        var rows = desktops.Select(desktop => {
             var totalCores = desktop.Cpus?.Sum(c => c.Cores) ?? 0;
             var totalThreads = desktop.Cpus?.Sum(c => c.Threads) ?? 0;
 
@@ -53,8 +50,7 @@ public class DesktopHardwareReportUseCase(IResourceCollection repository) : IUse
                     desktop.Nics
                         .GroupBy(n => n.Speed ?? 0)
                         .OrderBy(g => g.Key)
-                        .Select(g =>
-                        {
+                        .Select(g => {
                             var count = g.Sum(n => n.Ports ?? 0);
                             return $"{count}×{g.Key}G";
                         }));
@@ -82,4 +78,4 @@ public class DesktopHardwareReportUseCase(IResourceCollection repository) : IUse
 
         return new DesktopHardwareReport(rows);
     }
-}
+}

+ 11 - 25
RackPeek.Domain/Resources/Desktops/UpdateDesktopUseCase.cs

@@ -4,16 +4,14 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Desktops;
 
-public class UpdateDesktopUseCase(IResourceCollection repository) : IUseCase
-{
+public class UpdateDesktopUseCase(IResourceCollection repository) : IUseCase {
     public async Task ExecuteAsync(
         string name,
         string? model = null,
         double? ramGb = null,
         int? ramMts = null,
         string? notes = null
-    )
-    {
+    ) {
         // ToDo validate / normalize all inputs
 
         name = Normalize.HardwareName(name);
@@ -27,38 +25,26 @@ public class UpdateDesktopUseCase(IResourceCollection repository) : IUseCase
             desktop.Model = model;
 
         // ---- RAM ----
-        if (ramGb.HasValue)
-        {
+        if (ramGb.HasValue) {
             ThrowIfInvalid.RamGb(ramGb);
             desktop.Ram ??= new Ram();
             desktop.Ram.Size = ramGb.Value;
         }
 
-        if (ramMts.HasValue)
-        {
+        if (ramMts.HasValue) {
             desktop.Ram ??= new Ram();
             desktop.Ram.Mts = ramMts.Value;
         }
-        
-        if (desktop.Ram != null)
-        {
-            if (desktop.Ram.Size == 0)
-            {
-                desktop.Ram.Size = null;
-            }
-            
-            if (desktop.Ram.Mts == 0)
-            {
-                desktop.Ram.Mts = null;
-            }
 
-            if (desktop.Ram.Size == null && desktop.Ram.Mts == null)
-            {
-                desktop.Ram = null;
-            }
+        if (desktop.Ram != null) {
+            if (desktop.Ram.Size == 0) desktop.Ram.Size = null;
+
+            if (desktop.Ram.Mts == 0) desktop.Ram.Mts = null;
+
+            if (desktop.Ram.Size == null && desktop.Ram.Mts == null) desktop.Ram = null;
         }
 
         if (notes != null) desktop.Notes = notes;
         await repository.UpdateAsync(desktop);
     }
-}
+}

+ 6 - 9
RackPeek.Domain/Resources/Firewalls/DescribeFirewallUseCase.cs

@@ -15,10 +15,8 @@ public record FirewallDescription(
     Dictionary<string, string> Labels
 );
 
-public class DescribeFirewallUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<FirewallDescription> ExecuteAsync(string name)
-    {
+public class DescribeFirewallUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<FirewallDescription> ExecuteAsync(string name) {
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
@@ -27,7 +25,7 @@ public class DescribeFirewallUseCase(IResourceCollection repository) : IUseCase
             throw new NotFoundException($"Firewall '{name}' not found.");
 
         // If no ports exist, return defaults
-        var ports = firewallResource.Ports ?? new List<Port>();
+        List<Port> ports = firewallResource.Ports ?? new List<Port>();
 
         // Total ports count
         var totalPorts = ports.Sum(p => p.Count ?? 0);
@@ -36,10 +34,9 @@ public class DescribeFirewallUseCase(IResourceCollection repository) : IUseCase
         var totalSpeedGb = ports.Sum(p => (p.Speed ?? 0) * (p.Count ?? 0));
 
         // Build a port summary string
-        var portGroups = ports
+        IEnumerable<string> portGroups = ports
             .GroupBy(p => p.Type ?? "Unknown")
-            .Select(g =>
-            {
+            .Select(g => {
                 var count = g.Sum(x => x.Count ?? 0);
                 var speed = g.Sum(x => (x.Speed ?? 0) * (x.Count ?? 0));
                 return $"{g.Key}: {count} ports ({speed} Gb total)";
@@ -58,4 +55,4 @@ public class DescribeFirewallUseCase(IResourceCollection repository) : IUseCase
             firewallResource.Labels
         );
     }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/Firewalls/Firewall.cs

@@ -3,11 +3,10 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Firewalls;
 
-public class Firewall : Hardware.Hardware, IPortResource
-{
+public class Firewall : Hardware.Hardware, IPortResource {
     public const string KindLabel = "Firewall";
     public string? Model { get; set; }
     public bool? Managed { get; set; }
     public bool? Poe { get; set; }
     public List<Port>? Ports { get; set; }
-}
+}

+ 7 - 11
RackPeek.Domain/Resources/Firewalls/FirewallHardwareReport.cs

@@ -16,14 +16,11 @@ public record FirewallHardwareRow(
     string PortSummary
 );
 
-public class FirewallHardwareReportUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<FirewallHardwareReport> ExecuteAsync()
-    {
-        var firewalls = await repository.GetAllOfTypeAsync<Firewall>();
-
-        var rows = firewalls.Select(sw =>
-        {
+public class FirewallHardwareReportUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<FirewallHardwareReport> ExecuteAsync() {
+        IReadOnlyList<Firewall> firewalls = await repository.GetAllOfTypeAsync<Firewall>();
+
+        var rows = firewalls.Select(sw => {
             var totalPorts = sw.Ports?.Sum(p => p.Count ?? 0) ?? 0;
 
             var maxSpeed = sw.Ports?
@@ -35,8 +32,7 @@ public class FirewallHardwareReportUseCase(IResourceCollection repository) : IUs
                     sw.Ports
                         .GroupBy(p => p.Speed ?? 0)
                         .OrderBy(g => g.Key)
-                        .Select(g =>
-                        {
+                        .Select(g => {
                             var count = g.Sum(p => p.Count ?? 0);
                             return $"{count}×{g.Key}G";
                         }));
@@ -54,4 +50,4 @@ public class FirewallHardwareReportUseCase(IResourceCollection repository) : IUs
 
         return new FirewallHardwareReport(rows);
     }
-}
+}

+ 3 - 5
RackPeek.Domain/Resources/Firewalls/UpdateFirewallUseCase.cs

@@ -3,16 +3,14 @@ using RackPeek.Domain.Persistence;
 
 namespace RackPeek.Domain.Resources.Firewalls;
 
-public class UpdateFirewallUseCase(IResourceCollection repository) : IUseCase
-{
+public class UpdateFirewallUseCase(IResourceCollection repository) : IUseCase {
     public async Task ExecuteAsync(
         string name,
         string? model = null,
         bool? managed = null,
         bool? poe = null,
         string? notes = null
-    )
-    {
+    ) {
         // ToDo validate / normalize all inputs
 
         name = Normalize.HardwareName(name);
@@ -33,4 +31,4 @@ public class UpdateFirewallUseCase(IResourceCollection repository) : IUseCase
         if (notes != null) firewallResource.Notes = notes;
         await repository.UpdateAsync(firewallResource);
     }
-}
+}

+ 10 - 17
RackPeek.Domain/Resources/Hardware/GetHardwareSystemTreeUseCase.cs

@@ -1,15 +1,12 @@
 using RackPeek.Domain.Helpers;
 using RackPeek.Domain.Persistence;
-using RackPeek.Domain.Resources.Services;
 using RackPeek.Domain.Resources.SystemResources;
 
 namespace RackPeek.Domain.Resources.Hardware;
 
 public class GetHardwareSystemTreeUseCase(
-    IResourceCollection repo) : IUseCase
-{
-    public async Task<HardwareDependencyTree> ExecuteAsync(string hardwareName)
-    {
+    IResourceCollection repo) : IUseCase {
+    public async Task<HardwareDependencyTree> ExecuteAsync(string hardwareName) {
         ThrowIfInvalid.ResourceName(hardwareName);
 
         var hardware = await repo.GetByNameAsync(hardwareName) as Hardware;
@@ -19,33 +16,29 @@ public class GetHardwareSystemTreeUseCase(
         return await BuildDependencyTreeAsync(hardware);
     }
 
-    private async Task<HardwareDependencyTree> BuildDependencyTreeAsync(Hardware hardware)
-    {
-        var systems = await repo.GetDependantsAsync(hardware.Name);
+    private async Task<HardwareDependencyTree> BuildDependencyTreeAsync(Hardware hardware) {
+        IReadOnlyList<Resource> systems = await repo.GetDependantsAsync(hardware.Name);
 
         var systemTrees = new List<SystemDependencyTree>();
-        foreach (var system in systems.OfType<SystemResource>())
+        foreach (SystemResource system in systems.OfType<SystemResource>())
             systemTrees.Add(await BuildSystemDependencyTreeAsync(system));
 
         return new HardwareDependencyTree(hardware, systemTrees);
     }
 
-    private async Task<SystemDependencyTree> BuildSystemDependencyTreeAsync(SystemResource system)
-    {
-        var services = await repo.GetDependantsAsync(system.Name);
+    private async Task<SystemDependencyTree> BuildSystemDependencyTreeAsync(SystemResource system) {
+        IReadOnlyList<Resource> services = await repo.GetDependantsAsync(system.Name);
 
         return new SystemDependencyTree(system, services);
     }
 }
 
-public sealed class HardwareDependencyTree(Hardware hardware, IEnumerable<SystemDependencyTree> systems)
-{
+public sealed class HardwareDependencyTree(Hardware hardware, IEnumerable<SystemDependencyTree> systems) {
     public Hardware Hardware { get; } = hardware;
     public IEnumerable<SystemDependencyTree> Systems { get; } = systems;
 }
 
-public sealed class SystemDependencyTree(SystemResource system, IEnumerable<Resource> childResources)
-{
+public sealed class SystemDependencyTree(SystemResource system, IEnumerable<Resource> childResources) {
     public SystemResource System { get; } = system;
     public IEnumerable<Resource> ChildResources { get; } = childResources;
-}
+}

+ 7 - 11
RackPeek.Domain/Resources/Hardware/GetHardwareUseCaseSummary.cs

@@ -1,11 +1,9 @@
 namespace RackPeek.Domain.Resources.Hardware;
 
-public sealed class HardwareSummary
-{
+public sealed class HardwareSummary {
     public HardwareSummary(
         int totalHardware,
-        IReadOnlyDictionary<string, int> hardwareByKind)
-    {
+        IReadOnlyDictionary<string, int> hardwareByKind) {
         TotalHardware = totalHardware;
         HardwareByKind = hardwareByKind;
     }
@@ -14,12 +12,10 @@ public sealed class HardwareSummary
     public IReadOnlyDictionary<string, int> HardwareByKind { get; }
 }
 
-public class GetHardwareUseCaseSummary(IHardwareRepository repository) : IUseCase
-{
-    public async Task<HardwareSummary> ExecuteAsync()
-    {
-        var totalCountTask = repository.GetCountAsync();
-        var kindCountTask = repository.GetKindCountAsync();
+public class GetHardwareUseCaseSummary(IHardwareRepository repository) : IUseCase {
+    public async Task<HardwareSummary> ExecuteAsync() {
+        Task<int> totalCountTask = repository.GetCountAsync();
+        Task<Dictionary<string, int>> kindCountTask = repository.GetKindCountAsync();
 
         await Task.WhenAll(totalCountTask, kindCountTask);
 
@@ -28,4 +24,4 @@ public class GetHardwareUseCaseSummary(IHardwareRepository repository) : IUseCas
             kindCountTask.Result
         );
     }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/Hardware/Hardware.cs

@@ -1,5 +1,4 @@
 namespace RackPeek.Domain.Resources.Hardware;
 
-public abstract class Hardware : Resource
-{
-}
+public abstract class Hardware : Resource {
+}

+ 4 - 7
RackPeek.Domain/Resources/Hardware/IHardwareRepository.cs

@@ -1,22 +1,19 @@
 namespace RackPeek.Domain.Resources.Hardware;
 
-public interface IHardwareRepository
-{
+public interface IHardwareRepository {
     Task<int> GetCountAsync();
     Task<Dictionary<string, int>> GetKindCountAsync();
 
     public Task<List<HardwareTree>> GetTreeAsync();
 }
 
-public class HardwareTree
-{
+public class HardwareTree {
     public required string HardwareName { get; set; }
     public required string Kind { get; set; }
     public required List<SystemTree> Systems { get; set; }
 }
 
-public class SystemTree
-{
+public class SystemTree {
     public required string SystemName { get; set; }
     public required List<string> Services { get; set; }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/IResourceRepository.cs

@@ -1,5 +1,4 @@
 namespace RackPeek.Domain.Resources;
 
-public interface IResourceRepository
-{
-}
+public interface IResourceRepository {
+}

+ 3 - 5
RackPeek.Domain/Resources/Laptops/DescribeLaptopUseCase.cs

@@ -3,10 +3,8 @@ using RackPeek.Domain.Persistence;
 
 namespace RackPeek.Domain.Resources.Laptops;
 
-public class DescribeLaptopUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<LaptopDescription> ExecuteAsync(string name)
-    {
+public class DescribeLaptopUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<LaptopDescription> ExecuteAsync(string name) {
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
@@ -36,4 +34,4 @@ public record LaptopDescription(
     int DriveCount,
     int GpuCount,
     Dictionary<string, string> Labels
-);
+);

+ 2 - 3
RackPeek.Domain/Resources/Laptops/Laptop.cs

@@ -3,12 +3,11 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Laptops;
 
-public class Laptop : Hardware.Hardware, ICpuResource, IDriveResource, IGpuResource
-{
+public class Laptop : Hardware.Hardware, ICpuResource, IDriveResource, IGpuResource {
     public const string KindLabel = "Laptop";
     public Ram? Ram { get; set; }
     public string? Model { get; set; }
     public List<Cpu>? Cpus { get; set; }
     public List<Drive>? Drives { get; set; }
     public List<Gpu>? Gpus { get; set; }
-}
+}

+ 5 - 8
RackPeek.Domain/Resources/Laptops/LaptopHardwareReportUseCase.cs

@@ -2,14 +2,11 @@ using RackPeek.Domain.Persistence;
 
 namespace RackPeek.Domain.Resources.Laptops;
 
-public class LaptopHardwareReportUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<LaptopHardwareReport> ExecuteAsync()
-    {
-        var laptops = await repository.GetAllOfTypeAsync<Laptop>();
+public class LaptopHardwareReportUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<LaptopHardwareReport> ExecuteAsync() {
+        IReadOnlyList<Laptop> laptops = await repository.GetAllOfTypeAsync<Laptop>();
 
-        var rows = laptops.Select(laptop =>
-        {
+        var rows = laptops.Select(laptop => {
             var totalCores = laptop.Cpus?.Sum(c => c.Cores) ?? 0;
             var totalThreads = laptop.Cpus?.Sum(c => c.Threads) ?? 0;
 
@@ -68,4 +65,4 @@ public record LaptopHardwareRow(
     int SsdStorageGb,
     int HddStorageGb,
     string GpuSummary
-);
+);

+ 11 - 25
RackPeek.Domain/Resources/Laptops/UpdateLaptopUseCase.cs

@@ -4,16 +4,14 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Laptops;
 
-public class UpdateLaptopUseCase(IResourceCollection repository) : IUseCase
-{
+public class UpdateLaptopUseCase(IResourceCollection repository) : IUseCase {
     public async Task ExecuteAsync(
         string name,
         string? model = null,
         double? ramGb = null,
         int? ramMts = null,
         string? notes = null
-    )
-    {
+    ) {
         // ToDo validate / normalize all inputs
 
         name = Normalize.HardwareName(name);
@@ -27,39 +25,27 @@ public class UpdateLaptopUseCase(IResourceCollection repository) : IUseCase
             laptop.Model = model;
 
         // ---- RAM ----
-        if (ramGb.HasValue)
-        {
+        if (ramGb.HasValue) {
             ThrowIfInvalid.RamGb(ramGb);
             laptop.Ram ??= new Ram();
             laptop.Ram.Size = ramGb.Value;
         }
 
-        if (ramMts.HasValue)
-        {
+        if (ramMts.HasValue) {
             laptop.Ram ??= new Ram();
             laptop.Ram.Mts = ramMts.Value;
         }
-        
-        if (laptop.Ram != null)
-        {
-            if (laptop.Ram.Size == 0)
-            {
-                laptop.Ram.Size = null;
-            }
-            
-            if (laptop.Ram.Mts == 0)
-            {
-                laptop.Ram.Mts = null;
-            }
 
-            if (laptop.Ram.Size == null && laptop.Ram.Mts == null)
-            {
-                laptop.Ram = null;
-            }
+        if (laptop.Ram != null) {
+            if (laptop.Ram.Size == 0) laptop.Ram.Size = null;
+
+            if (laptop.Ram.Mts == 0) laptop.Ram.Mts = null;
+
+            if (laptop.Ram.Size == null && laptop.Ram.Mts == null) laptop.Ram = null;
         }
 
 
         if (notes != null) laptop.Notes = notes;
         await repository.UpdateAsync(laptop);
     }
-}
+}

+ 28 - 40
RackPeek.Domain/Resources/Resource.cs

@@ -11,37 +11,11 @@ using RackPeek.Domain.Resources.UpsUnits;
 
 namespace RackPeek.Domain.Resources;
 
-public abstract class Resource
-{
-    private static readonly string[] HardwareTypes =
+public abstract class Resource {
+    private static readonly string[] _hardwareTypes =
         ["server", "switch", "firewall", "router", "accesspoint", "desktop", "laptop", "ups"];
 
-    public static bool IsHardware(string kind)
-    {
-        kind = kind.Trim().ToLower();
-        return kind == "hardware" || HardwareTypes.Contains(kind);
-    } 
-        
-    public static string GetResourceUrl(string kind, string name)
-    {
-        var encoded = Uri.EscapeDataString(name);
-
-        kind = kind.Trim().ToLower();
-        if (IsHardware(kind))
-        {
-            return $"resources/hardware/{encoded}";
-        }else if (kind == "system")
-        {
-            return $"resources/systems/{encoded}";
-        }else if (kind == "service")
-        {
-            return $"resources/services/{encoded}";
-        }
-
-        return "#";
-    }
-    
-    private static readonly Dictionary<string, string> KindToPluralDictionary = new()
+    private static readonly Dictionary<string, string> _kindToPluralDictionary = new()
     {
         { "hardware", "hardware" },
         { "server", "servers" },
@@ -56,7 +30,7 @@ public abstract class Resource
         { "service", "services" }
     };
 
-    private static readonly Dictionary<Type, string> TypeToKindMap = new()
+    private static readonly Dictionary<Type, string> _typeToKindMap = new()
     {
         { typeof(Hardware.Hardware), "Hardware" },
         { typeof(Server), "Server" },
@@ -79,24 +53,38 @@ public abstract class Resource
     public Dictionary<string, string> Labels { get; set; } = new();
     public string? Notes { get; set; }
 
-    public List<string> RunsOn { get; set; } = new List<string>();
+    public List<string> RunsOn { get; set; } = new();
 
-    public static string KindToPlural(string kind)
-    {
-        return KindToPluralDictionary.GetValueOrDefault(kind.ToLower().Trim(), kind);
+    public static bool IsHardware(string kind) {
+        kind = kind.Trim().ToLower();
+        return kind == "hardware" || _hardwareTypes.Contains(kind);
     }
 
-    public static string GetKind<T>() where T : Resource
-    {
-        if (TypeToKindMap.TryGetValue(typeof(T), out var kind))
+    public static string GetResourceUrl(string kind, string name) {
+        var encoded = Uri.EscapeDataString(name);
+
+        kind = kind.Trim().ToLower();
+        if (IsHardware(kind)) return $"resources/hardware/{encoded}";
+
+        if (kind == "system") return $"resources/systems/{encoded}";
+
+        if (kind == "service") return $"resources/services/{encoded}";
+
+        return "#";
+    }
+
+    public static string KindToPlural(string kind) =>
+        _kindToPluralDictionary.GetValueOrDefault(kind.ToLower().Trim(), kind);
+
+    public static string GetKind<T>() where T : Resource {
+        if (_typeToKindMap.TryGetValue(typeof(T), out var kind))
             return kind;
 
         throw new InvalidOperationException(
             $"No kind mapping defined for type {typeof(T).Name}");
     }
 
-    public static bool CanRunOn<T>(Resource parent) where T : Resource
-    {
+    public static bool CanRunOn<T>(Resource parent) where T : Resource {
         var childKind = GetKind<T>().ToLowerInvariant();
         var parentKind = parent.Kind.ToLowerInvariant();
 
@@ -107,7 +95,7 @@ public abstract class Resource
         // System -> Hardware
         if (childKind == "system" && parent is Hardware.Hardware)
             return true;
-        
+
         // System -> System
         if (childKind == "system" && parent is SystemResource)
             return true;

+ 6 - 9
RackPeek.Domain/Resources/Routers/DescribeRouterUseCase.cs

@@ -15,10 +15,8 @@ public record RouterDescription(
     Dictionary<string, string> Labels
 );
 
-public class DescribeRouterUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<RouterDescription> ExecuteAsync(string name)
-    {
+public class DescribeRouterUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<RouterDescription> ExecuteAsync(string name) {
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
@@ -27,7 +25,7 @@ public class DescribeRouterUseCase(IResourceCollection repository) : IUseCase
             throw new NotFoundException($"Router '{name}' not found.");
 
         // If no ports exist, return defaults
-        var ports = routerResource.Ports ?? new List<Port>();
+        List<Port> ports = routerResource.Ports ?? new List<Port>();
 
         // Total ports count
         var totalPorts = ports.Sum(p => p.Count ?? 0);
@@ -36,10 +34,9 @@ public class DescribeRouterUseCase(IResourceCollection repository) : IUseCase
         var totalSpeedGb = ports.Sum(p => (p.Speed ?? 0) * (p.Count ?? 0));
 
         // Build a port summary string
-        var portGroups = ports
+        IEnumerable<string> portGroups = ports
             .GroupBy(p => p.Type ?? "Unknown")
-            .Select(g =>
-            {
+            .Select(g => {
                 var count = g.Sum(x => x.Count ?? 0);
                 var speed = g.Sum(x => (x.Speed ?? 0) * (x.Count ?? 0));
                 return $"{g.Key}: {count} ports ({speed} Gb total)";
@@ -58,4 +55,4 @@ public class DescribeRouterUseCase(IResourceCollection repository) : IUseCase
             routerResource.Labels
         );
     }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/Routers/Router.cs

@@ -3,11 +3,10 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Routers;
 
-public class Router : Hardware.Hardware, IPortResource
-{
+public class Router : Hardware.Hardware, IPortResource {
     public const string KindLabel = "Router";
     public string? Model { get; set; }
     public bool? Managed { get; set; }
     public bool? Poe { get; set; }
     public List<Port>? Ports { get; set; }
-}
+}

+ 7 - 11
RackPeek.Domain/Resources/Routers/RouterHardwareReport.cs

@@ -16,14 +16,11 @@ public record RouterHardwareRow(
     string PortSummary
 );
 
-public class RouterHardwareReportUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<RouterHardwareReport> ExecuteAsync()
-    {
-        var routers = await repository.GetAllOfTypeAsync<Router>();
-
-        var rows = routers.Select(sw =>
-        {
+public class RouterHardwareReportUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<RouterHardwareReport> ExecuteAsync() {
+        IReadOnlyList<Router> routers = await repository.GetAllOfTypeAsync<Router>();
+
+        var rows = routers.Select(sw => {
             var totalPorts = sw.Ports?.Sum(p => p.Count ?? 0) ?? 0;
 
             var maxSpeed = sw.Ports?
@@ -35,8 +32,7 @@ public class RouterHardwareReportUseCase(IResourceCollection repository) : IUseC
                     sw.Ports
                         .GroupBy(p => p.Speed ?? 0)
                         .OrderBy(g => g.Key)
-                        .Select(g =>
-                        {
+                        .Select(g => {
                             var count = g.Sum(p => p.Count ?? 0);
                             return $"{count}×{g.Key}G";
                         }));
@@ -54,4 +50,4 @@ public class RouterHardwareReportUseCase(IResourceCollection repository) : IUseC
 
         return new RouterHardwareReport(rows);
     }
-}
+}

+ 3 - 5
RackPeek.Domain/Resources/Routers/UpdateRouterUseCase.cs

@@ -3,16 +3,14 @@ using RackPeek.Domain.Persistence;
 
 namespace RackPeek.Domain.Resources.Routers;
 
-public class UpdateRouterUseCase(IResourceCollection repository) : IUseCase
-{
+public class UpdateRouterUseCase(IResourceCollection repository) : IUseCase {
     public async Task ExecuteAsync(
         string name,
         string? model = null,
         bool? managed = null,
         bool? poe = null,
         string? notes = null
-    )
-    {
+    ) {
         // ToDo pass in properties as inputs, construct the entity in the usecase
         // ToDo validate / normalize all inputs
 
@@ -34,4 +32,4 @@ public class UpdateRouterUseCase(IResourceCollection repository) : IUseCase
         if (notes != null) routerResource.Notes = notes;
         await repository.UpdateAsync(routerResource);
     }
-}
+}

+ 3 - 5
RackPeek.Domain/Resources/Servers/DescribeServerUseCase.cs

@@ -14,10 +14,8 @@ public record ServerDescription(
     bool Ipmi
 );
 
-public class DescribeServerUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<ServerDescription> ExecuteAsync(string name)
-    {
+public class DescribeServerUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<ServerDescription> ExecuteAsync(string name) {
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
@@ -43,4 +41,4 @@ public class DescribeServerUseCase(IResourceCollection repository) : IUseCase
             server.Ipmi ?? false
         );
     }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/Servers/ICpuResource.cs

@@ -2,7 +2,6 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Servers;
 
-public interface ICpuResource
-{
+public interface ICpuResource {
     public List<Cpu>? Cpus { get; set; }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/Servers/IDriveResource.cs

@@ -2,7 +2,6 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Servers;
 
-public interface IDriveResource
-{
+public interface IDriveResource {
     public List<Drive>? Drives { get; set; }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/Servers/IGpuResource.cs

@@ -2,7 +2,6 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Servers;
 
-public interface IGpuResource
-{
+public interface IGpuResource {
     public List<Gpu>? Gpus { get; set; }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/Servers/INicResource.cs

@@ -2,7 +2,6 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Servers;
 
-public interface INicResource
-{
+public interface INicResource {
     public List<Nic>? Nics { get; set; }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/Servers/IPortResource.cs

@@ -2,7 +2,6 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Servers;
 
-public interface IPortResource
-{
+public interface IPortResource {
     public List<Port>? Ports { get; set; }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/Servers/Server.cs

@@ -2,8 +2,7 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Servers;
 
-public class Server : Hardware.Hardware, ICpuResource, IDriveResource, IGpuResource, INicResource
-{
+public class Server : Hardware.Hardware, ICpuResource, IDriveResource, IGpuResource, INicResource {
     public const string KindLabel = "Server";
     public Ram? Ram { get; set; }
     public bool? Ipmi { get; set; }
@@ -11,4 +10,4 @@ public class Server : Hardware.Hardware, ICpuResource, IDriveResource, IGpuResou
     public List<Drive>? Drives { get; set; }
     public List<Gpu>? Gpus { get; set; }
     public List<Nic>? Nics { get; set; }
-}
+}

+ 20 - 25
RackPeek.Domain/Resources/Servers/ServerHardwareReport.cs

@@ -21,34 +21,29 @@ public record ServerHardwareRow(
     int GpuCount,
     int TotalGpuVramGb,
     string GpuSummary,
-    bool Ipmi, 
+    bool Ipmi,
     IReadOnlyList<Nic> Nics
-)
-{        
-public string NicSummary =>
-string.Join(", ",
-    (Nics ?? [])
-    .SelectMany(n =>
-    {
-        var ports = n.Ports ?? 1;
-        var speed = n.Speed ?? 0;
-        return Enumerable.Repeat(speed, ports);
-    })
-    .GroupBy(speed => speed)
-    .OrderByDescending(g => g.Key)
-    .Select(g => $"{g.Count()}×{g.Key}G")
-    .DefaultIfEmpty("none")
-);
+) {
+    public string NicSummary =>
+        string.Join(", ",
+            (Nics ?? [])
+            .SelectMany(n => {
+                var ports = n.Ports ?? 1;
+                var speed = n.Speed ?? 0;
+                return Enumerable.Repeat(speed, ports);
+            })
+            .GroupBy(speed => speed)
+            .OrderByDescending(g => g.Key)
+            .Select(g => $"{g.Count()}×{g.Key}G")
+            .DefaultIfEmpty("none")
+        );
 }
 
-public class ServerHardwareReportUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<ServerHardwareReport> ExecuteAsync()
-    {
-        var servers = await repository.GetAllOfTypeAsync<Server>();
+public class ServerHardwareReportUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<ServerHardwareReport> ExecuteAsync() {
+        IReadOnlyList<Server> servers = await repository.GetAllOfTypeAsync<Server>();
 
-        var rows = servers.Select(server =>
-        {
+        var rows = servers.Select(server => {
             var totalCores = server.Cpus?.Sum(c => c.Cores) ?? 0;
             var totalThreads = server.Cpus?.Sum(c => c.Threads) ?? 0;
 
@@ -106,4 +101,4 @@ public class ServerHardwareReportUseCase(IResourceCollection repository) : IUseC
 
         return new ServerHardwareReport(rows);
     }
-}
+}

+ 10 - 24
RackPeek.Domain/Resources/Servers/UpdateServerUseCase.cs

@@ -4,16 +4,14 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Servers;
 
-public class UpdateServerUseCase(IResourceCollection repository) : IUseCase
-{
+public class UpdateServerUseCase(IResourceCollection repository) : IUseCase {
     public async Task ExecuteAsync(
         string name,
         double? ramGb = null,
         int? ramMts = null,
         bool? ipmi = null,
         string? notes = null
-    )
-    {
+    ) {
         // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
         // ToDo validate / normalize all inputs
 
@@ -25,35 +23,23 @@ public class UpdateServerUseCase(IResourceCollection repository) : IUseCase
             throw new NotFoundException($"Server '{name}' not found.");
 
         // ---- RAM ----
-        if (ramGb.HasValue)
-        {
+        if (ramGb.HasValue) {
             ThrowIfInvalid.RamGb(ramGb);
             server.Ram ??= new Ram();
             server.Ram.Size = ramGb.Value;
         }
 
-        if (ramMts.HasValue)
-        {
+        if (ramMts.HasValue) {
             server.Ram ??= new Ram();
             server.Ram.Mts = ramMts.Value;
         }
 
-        if (server.Ram != null)
-        {
-            if (server.Ram.Size == 0)
-            {
-                server.Ram.Size = null;
-            }
-            
-            if (server.Ram.Mts == 0)
-            {
-                server.Ram.Mts = null;
-            }
+        if (server.Ram != null) {
+            if (server.Ram.Size == 0) server.Ram.Size = null;
 
-            if (server.Ram.Size == null && server.Ram.Mts == null)
-            {
-                server.Ram = null;
-            }
+            if (server.Ram.Mts == 0) server.Ram.Mts = null;
+
+            if (server.Ram.Size == null && server.Ram.Mts == null) server.Ram = null;
         }
 
         // ---- IPMI ----
@@ -61,4 +47,4 @@ public class UpdateServerUseCase(IResourceCollection repository) : IUseCase
         if (notes != null) server.Notes = notes;
         await repository.UpdateAsync(server);
     }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/Services/IServiceRepository.cs

@@ -1,9 +1,8 @@
 namespace RackPeek.Domain.Resources.Services;
 
-public interface IServiceRepository
-{
+public interface IServiceRepository {
     Task<int> GetCountAsync();
     Task<int> GetIpAddressCountAsync();
 
     Task<IReadOnlyList<Service>> GetBySystemHostAsync(string name);
-}
+}

+ 6 - 15
RackPeek.Domain/Resources/Services/Networking/Cidr.cs

@@ -1,30 +1,21 @@
 namespace RackPeek.Domain.Resources.Services.Networking;
 
-public readonly struct Cidr
-{
+public readonly struct Cidr {
     public uint Network { get; }
     public uint Mask { get; }
     public int Prefix { get; }
 
-    public Cidr(uint network, uint mask, int prefix)
-    {
+    public Cidr(uint network, uint mask, int prefix) {
         Network = network;
         Mask = mask;
         Prefix = prefix;
     }
 
-    public bool Contains(uint ip)
-    {
-        return (ip & Mask) == Network;
-    }
+    public bool Contains(uint ip) => (ip & Mask) == Network;
 
-    public override string ToString()
-    {
-        return $"{IpHelper.ToIp(Network)}/{Prefix}";
-    }
+    public override string ToString() => $"{IpHelper.ToIp(Network)}/{Prefix}";
 
-    public static Cidr Parse(string cidr)
-    {
+    public static Cidr Parse(string cidr) {
         var parts = cidr.Split('/');
         if (parts.Length != 2)
             throw new ArgumentException($"CIDR must be in format a.b.c.d/nn: {cidr}");
@@ -37,4 +28,4 @@ public readonly struct Cidr
 
         return new Cidr(network, mask, prefix);
     }
-}
+}

+ 5 - 9
RackPeek.Domain/Resources/Services/Networking/IpHelper.cs

@@ -1,9 +1,7 @@
 namespace RackPeek.Domain.Resources.Services.Networking;
 
-public static class IpHelper
-{
-    public static uint ToUInt32(string ip)
-    {
+public static class IpHelper {
+    public static uint ToUInt32(string ip) {
         var parts = ip.Split('.');
         if (parts.Length != 4)
             throw new ArgumentException($"Invalid IPv4 address: {ip}");
@@ -15,8 +13,7 @@ public static class IpHelper
             int.Parse(parts[3]));
     }
 
-    public static string ToIp(uint ip)
-    {
+    public static string ToIp(uint ip) {
         return string.Join('.',
             (ip >> 24) & 0xFF,
             (ip >> 16) & 0xFF,
@@ -24,11 +21,10 @@ public static class IpHelper
             ip & 0xFF);
     }
 
-    public static uint MaskFromPrefix(int prefix)
-    {
+    public static uint MaskFromPrefix(int prefix) {
         if (prefix < 0 || prefix > 32)
             throw new ArgumentException($"Invalid CIDR prefix: {prefix}");
 
         return prefix == 0 ? 0 : uint.MaxValue << (32 - prefix);
     }
-}
+}

+ 6 - 11
RackPeek.Domain/Resources/Services/Service.cs

@@ -2,24 +2,20 @@ using System.Text;
 
 namespace RackPeek.Domain.Resources.Services;
 
-public class Service : Resource
-{
+public class Service : Resource {
     public const string KindLabel = "Service";
     public Network? Network { get; set; }
 
-    public string NetworkString()
-    {
+    public string NetworkString() {
         if (Network == null) return string.Empty;
 
         if (!string.IsNullOrEmpty(Network.Url)) return Network.Url;
 
         var stringBuilder = new StringBuilder();
-        if (!string.IsNullOrEmpty(Network.Ip))
-        {
+        if (!string.IsNullOrEmpty(Network.Ip)) {
             stringBuilder.Append("Ip: ");
             stringBuilder.Append(Network.Ip);
-            if (Network.Port.HasValue)
-            {
+            if (Network.Port.HasValue) {
                 stringBuilder.Append(':');
                 stringBuilder.Append(Network.Port.Value);
             }
@@ -31,10 +27,9 @@ public class Service : Resource
     }
 }
 
-public class Network
-{
+public class Network {
     public string? Ip { get; set; }
     public int? Port { get; set; }
     public string? Protocol { get; set; }
     public string? Url { get; set; }
-}
+}

+ 5 - 14
RackPeek.Domain/Resources/Services/UseCases/DescribeServiceUseCase.cs

@@ -15,30 +15,21 @@ public record ServiceDescription(
     Dictionary<string, string> Labels
 );
 
-public class DescribeServiceUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<ServiceDescription> ExecuteAsync(string name)
-    {
+public class DescribeServiceUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<ServiceDescription> ExecuteAsync(string name) {
         name = Normalize.ServiceName(name);
         ThrowIfInvalid.ResourceName(name);
         var service = await repository.GetByNameAsync(name) as Service;
         if (service is null)
             throw new NotFoundException($"Service '{name}' not found.");
 
-        List<string> runsOnPhysicalHost = new List<string>();
-        foreach (var systemName in service.RunsOn)
-        {
+        var runsOnPhysicalHost = new List<string>();
+        foreach (var systemName in service.RunsOn) {
             var systemResource = await repository.GetByNameAsync(systemName) as SystemResource;
             if (systemResource is not null)
-            {
-                foreach(var physicalName in systemResource.RunsOn)
-                {
+                foreach (var physicalName in systemResource.RunsOn)
                     if (!runsOnPhysicalHost.Contains(physicalName))
-                    {
                         runsOnPhysicalHost.Add(physicalName);
-                    }
-                }
-            }
         }
 
         return new ServiceDescription(

+ 6 - 9
RackPeek.Domain/Resources/Services/UseCases/GetServiceSummaryUseCase.cs

@@ -1,17 +1,14 @@
 namespace RackPeek.Domain.Resources.Services.UseCases;
 
-public sealed class AllServicesSummary(int totalServices, int totalIpAddresses)
-{
+public sealed class AllServicesSummary(int totalServices, int totalIpAddresses) {
     public int TotalServices { get; } = totalServices;
     public int TotalIpAddresses { get; } = totalIpAddresses;
 }
 
-public class GetServiceSummaryUseCase(IServiceRepository repository) : IUseCase
-{
-    public async Task<AllServicesSummary> ExecuteAsync()
-    {
-        var serviceCountTask = repository.GetCountAsync();
-        var ipAddressCountTask = repository.GetIpAddressCountAsync();
+public class GetServiceSummaryUseCase(IServiceRepository repository) : IUseCase {
+    public async Task<AllServicesSummary> ExecuteAsync() {
+        Task<int> serviceCountTask = repository.GetCountAsync();
+        Task<int> ipAddressCountTask = repository.GetIpAddressCountAsync();
 
         await Task.WhenAll(serviceCountTask, ipAddressCountTask);
 
@@ -20,4 +17,4 @@ public class GetServiceSummaryUseCase(IServiceRepository repository) : IUseCase
             ipAddressCountTask.Result
         );
     }
-}
+}

+ 10 - 19
RackPeek.Domain/Resources/Services/UseCases/ServiceReportUseCase.cs

@@ -16,29 +16,20 @@ public record ServiceReportRow(
     List<string>? RunsOnPhysicalHost
 );
 
-public class ServiceReportUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<ServiceReport> ExecuteAsync()
-    {
-        var services = await repository.GetAllOfTypeAsync<Service>();
+public class ServiceReportUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<ServiceReport> ExecuteAsync() {
+        IReadOnlyList<Service> services = await repository.GetAllOfTypeAsync<Service>();
 
-        var rows = services.Select(async s =>
-        {
-            List<string> runsOnPhysicalHost = new List<string>();
+        var rows = services.Select(async s => {
+            var runsOnPhysicalHost = new List<string>();
             if (s.RunsOn is not null)
-            {
-                foreach (var system in s.RunsOn)
-                {
-                    var systemResource = await repository.GetByNameAsync(system);
+                foreach (var system in s.RunsOn) {
+                    Resource? systemResource = await repository.GetByNameAsync(system);
                     if (systemResource?.RunsOn is not null)
-                    {
                         foreach (var parent in systemResource.RunsOn)
-                        {
-                            if (!runsOnPhysicalHost.Contains(parent)) runsOnPhysicalHost.Add(parent);
-                        }
-                    }
+                            if (!runsOnPhysicalHost.Contains(parent))
+                                runsOnPhysicalHost.Add(parent);
                 }
-            }
 
             return new ServiceReportRow(
                 s.Name,
@@ -51,7 +42,7 @@ public class ServiceReportUseCase(IResourceCollection repository) : IUseCase
             );
         }).ToList();
 
-        var result = await Task.WhenAll(rows);
+        ServiceReportRow[] result = await Task.WhenAll(rows);
         return new ServiceReport(result);
     }
 }

+ 12 - 25
RackPeek.Domain/Resources/Services/UseCases/ServiceSubnetsUseCase.cs

@@ -3,22 +3,17 @@ using RackPeek.Domain.Resources.Services.Networking;
 
 namespace RackPeek.Domain.Resources.Services.UseCases;
 
-public class ServiceSubnetsUseCase(IResourceCollection repo) : IUseCase
-{
-    public async Task<ServiceSubnetsResult> ExecuteAsync(string? cidr, int? prefix, CancellationToken token)
-    {
-        var services = await repo.GetAllOfTypeAsync<Service>();
+public class ServiceSubnetsUseCase(IResourceCollection repo) : IUseCase {
+    public async Task<ServiceSubnetsResult> ExecuteAsync(string? cidr, int? prefix, CancellationToken token) {
+        IReadOnlyList<Service> services = await repo.GetAllOfTypeAsync<Service>();
 
         // If CIDR is provided → filter mode
-        if (cidr is not null)
-        {
+        if (cidr is not null) {
             Cidr parsed;
-            try
-            {
+            try {
                 parsed = Cidr.Parse(cidr);
             }
-            catch
-            {
+            catch {
                 return ServiceSubnetsResult.InvalidCidr(cidr);
             }
 
@@ -56,8 +51,7 @@ public record SubnetSummary(string Cidr, int Count);
 
 public record ServiceSummary(string Name, string Ip, List<string>? RunsOn);
 
-public class ServiceSubnetsResult
-{
+public class ServiceSubnetsResult {
     public bool IsInvalidCidr { get; private set; }
     public string? InvalidCidrValue { get; private set; }
 
@@ -66,18 +60,11 @@ public class ServiceSubnetsResult
     public List<SubnetSummary> Subnets { get; private set; } = new();
     public List<ServiceSummary> Services { get; private set; } = new();
 
-    public static ServiceSubnetsResult InvalidCidr(string cidr)
-    {
-        return new ServiceSubnetsResult { IsInvalidCidr = true, InvalidCidrValue = cidr };
-    }
+    public static ServiceSubnetsResult InvalidCidr(string cidr) =>
+        new() { IsInvalidCidr = true, InvalidCidrValue = cidr };
 
-    public static ServiceSubnetsResult FromSubnets(List<SubnetSummary> subnets)
-    {
-        return new ServiceSubnetsResult { Subnets = subnets };
-    }
+    public static ServiceSubnetsResult FromSubnets(List<SubnetSummary> subnets) => new() { Subnets = subnets };
 
-    public static ServiceSubnetsResult FromServices(List<ServiceSummary> services, string cidr)
-    {
-        return new ServiceSubnetsResult { Services = services, FilteredCidr = cidr };
-    }
+    public static ServiceSubnetsResult FromServices(List<ServiceSummary> services, string cidr) =>
+        new() { Services = services, FilteredCidr = cidr };
 }

+ 9 - 17
RackPeek.Domain/Resources/Services/UseCases/UpdateServiceUseCase.cs

@@ -3,8 +3,7 @@ using RackPeek.Domain.Persistence;
 
 namespace RackPeek.Domain.Resources.Services.UseCases;
 
-public class UpdateServiceUseCase(IResourceCollection repository) : IUseCase
-{
+public class UpdateServiceUseCase(IResourceCollection repository) : IUseCase {
     public async Task ExecuteAsync(
         string name,
         string? ip = null,
@@ -13,8 +12,7 @@ public class UpdateServiceUseCase(IResourceCollection repository) : IUseCase
         string? url = null,
         List<string>? runsOn = null,
         string? notes = null
-    )
-    {
+    ) {
         // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
         // ToDo validate / normalize all inputs
 
@@ -24,42 +22,36 @@ public class UpdateServiceUseCase(IResourceCollection repository) : IUseCase
         if (service is null)
             throw new NotFoundException($"Service '{name}' not found.");
 
-        if (ip != null)
-        {
+        if (ip != null) {
             service.Network ??= new Network();
             service.Network.Ip = ip;
         }
 
-        if (protocol != null)
-        {
+        if (protocol != null) {
             service.Network ??= new Network();
             service.Network.Protocol = protocol;
         }
 
-        if (url != null)
-        {
+        if (url != null) {
             service.Network ??= new Network();
             service.Network.Url = url;
         }
 
-        if (port.HasValue)
-        {
+        if (port.HasValue) {
             service.Network ??= new Network();
             service.Network.Port = port.Value;
         }
 
-        if (runsOn is not null)
-        {
+        if (runsOn is not null) {
             var normalizedParents = new List<string>();
 
             foreach (var parent in runsOn
                          .Where(p => !string.IsNullOrWhiteSpace(p))
                          .Select(p => p.Trim())
-                         .Distinct(StringComparer.OrdinalIgnoreCase))
-            {
+                         .Distinct(StringComparer.OrdinalIgnoreCase)) {
                 ThrowIfInvalid.ResourceName(parent);
 
-                var parentSystem = await repository.GetByNameAsync(parent);
+                Resource? parentSystem = await repository.GetByNameAsync(parent);
 
                 if (parentSystem == null)
                     throw new NotFoundException($"Parent system '{parent}' not found.");

+ 3 - 7
RackPeek.Domain/Resources/SubResources/Cpu.cs

@@ -1,13 +1,9 @@
 namespace RackPeek.Domain.Resources.SubResources;
 
-public class Cpu
-{
+public class Cpu {
     public string? Model { get; set; }
     public int? Cores { get; set; }
     public int? Threads { get; set; }
 
-    public override string ToString()
-    {
-        return $"{Model} {Cores} {Threads}";
-    }
-}
+    public override string ToString() => $"{Model} {Cores} {Threads}";
+}

+ 2 - 3
RackPeek.Domain/Resources/SubResources/Drive.cs

@@ -1,7 +1,6 @@
 namespace RackPeek.Domain.Resources.SubResources;
 
-public class Drive
-{
+public class Drive {
     public static readonly string[] ValidDriveTypes =
     {
         // Flash storage
@@ -16,4 +15,4 @@ public class Drive
 
     public string? Type { get; set; }
     public int? Size { get; set; }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/SubResources/Gpu.cs

@@ -1,7 +1,6 @@
 namespace RackPeek.Domain.Resources.SubResources;
 
-public class Gpu
-{
+public class Gpu {
     public string? Model { get; set; }
     public int? Vram { get; set; }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/SubResources/Nic.cs

@@ -1,7 +1,6 @@
 namespace RackPeek.Domain.Resources.SubResources;
 
-public class Nic
-{
+public class Nic {
     public static readonly string[] ValidNicTypes =
     {
         // Copper Ethernet
@@ -32,4 +31,4 @@ public class Nic
     public string? Type { get; set; }
     public double? Speed { get; set; }
     public int? Ports { get; set; }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/SubResources/Port.cs

@@ -1,8 +1,7 @@
 namespace RackPeek.Domain.Resources.SubResources;
 
-public class Port
-{
+public class Port {
     public string? Type { get; set; }
     public double? Speed { get; set; }
     public int? Count { get; set; }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/SubResources/Ram.cs

@@ -1,7 +1,6 @@
 namespace RackPeek.Domain.Resources.SubResources;
 
-public class Ram
-{
+public class Ram {
     public double? Size { get; set; }
     public int? Mts { get; set; }
-}
+}

+ 6 - 9
RackPeek.Domain/Resources/Switches/DescribeSwitchUseCase.cs

@@ -15,10 +15,8 @@ public record SwitchDescription(
     Dictionary<string, string> Labels
 );
 
-public class DescribeSwitchUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<SwitchDescription> ExecuteAsync(string name)
-    {
+public class DescribeSwitchUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<SwitchDescription> ExecuteAsync(string name) {
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
@@ -27,7 +25,7 @@ public class DescribeSwitchUseCase(IResourceCollection repository) : IUseCase
             throw new NotFoundException($"Switch '{name}' not found.");
 
         // If no ports exist, return defaults
-        var ports = switchResource.Ports ?? new List<Port>();
+        List<Port> ports = switchResource.Ports ?? new List<Port>();
 
         // Total ports count
         var totalPorts = ports.Sum(p => p.Count ?? 0);
@@ -36,10 +34,9 @@ public class DescribeSwitchUseCase(IResourceCollection repository) : IUseCase
         var totalSpeedGb = ports.Sum(p => (p.Speed ?? 0) * (p.Count ?? 0));
 
         // Build a port summary string
-        var portGroups = ports
+        IEnumerable<string> portGroups = ports
             .GroupBy(p => p.Type ?? "Unknown")
-            .Select(g =>
-            {
+            .Select(g => {
                 var count = g.Sum(x => x.Count ?? 0);
                 var speed = g.Sum(x => (x.Speed ?? 0) * (x.Count ?? 0));
                 return $"{g.Key}: {count} ports ({speed} Gb total)";
@@ -58,4 +55,4 @@ public class DescribeSwitchUseCase(IResourceCollection repository) : IUseCase
             switchResource.Labels
         );
     }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/Switches/Switch.cs

@@ -3,11 +3,10 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Switches;
 
-public class Switch : Hardware.Hardware, IPortResource
-{
+public class Switch : Hardware.Hardware, IPortResource {
     public const string KindLabel = "Switch";
     public string? Model { get; set; }
     public bool? Managed { get; set; }
     public bool? Poe { get; set; }
     public List<Port>? Ports { get; set; }
-}
+}

+ 7 - 11
RackPeek.Domain/Resources/Switches/SwitchHardwareReport.cs

@@ -16,14 +16,11 @@ public record SwitchHardwareRow(
     string PortSummary
 );
 
-public class SwitchHardwareReportUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<SwitchHardwareReport> ExecuteAsync()
-    {
-        var switches = await repository.GetAllOfTypeAsync<Switch>();
-
-        var rows = switches.Select(sw =>
-        {
+public class SwitchHardwareReportUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<SwitchHardwareReport> ExecuteAsync() {
+        IReadOnlyList<Switch> switches = await repository.GetAllOfTypeAsync<Switch>();
+
+        var rows = switches.Select(sw => {
             var totalPorts = sw.Ports?.Sum(p => p.Count ?? 0) ?? 0;
 
             var maxSpeed = sw.Ports?
@@ -35,8 +32,7 @@ public class SwitchHardwareReportUseCase(IResourceCollection repository) : IUseC
                     sw.Ports
                         .GroupBy(p => p.Speed ?? 0)
                         .OrderBy(g => g.Key)
-                        .Select(g =>
-                        {
+                        .Select(g => {
                             var count = g.Sum(p => p.Count ?? 0);
                             return $"{count}×{g.Key}G";
                         }));
@@ -54,4 +50,4 @@ public class SwitchHardwareReportUseCase(IResourceCollection repository) : IUseC
 
         return new SwitchHardwareReport(rows);
     }
-}
+}

+ 3 - 5
RackPeek.Domain/Resources/Switches/UpdateSwitchUseCase.cs

@@ -3,16 +3,14 @@ using RackPeek.Domain.Persistence;
 
 namespace RackPeek.Domain.Resources.Switches;
 
-public class UpdateSwitchUseCase(IResourceCollection repository) : IUseCase
-{
+public class UpdateSwitchUseCase(IResourceCollection repository) : IUseCase {
     public async Task ExecuteAsync(
         string name,
         string? model = null,
         bool? managed = null,
         bool? poe = null,
         string? notes = null
-    )
-    {
+    ) {
         // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
         // ToDo validate / normalize all inputs
 
@@ -34,4 +32,4 @@ public class UpdateSwitchUseCase(IResourceCollection repository) : IUseCase
         if (notes != null) switchResource.Notes = notes;
         await repository.UpdateAsync(switchResource);
     }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/SystemResources/ISystemRepository.cs

@@ -1,10 +1,9 @@
 namespace RackPeek.Domain.Resources.SystemResources;
 
-public interface ISystemRepository
-{
+public interface ISystemRepository {
     Task<int> GetSystemCountAsync();
     Task<Dictionary<string, int>> GetSystemTypeCountAsync();
     Task<Dictionary<string, int>> GetSystemOsCountAsync();
     Task<IReadOnlyList<SystemResource>> GetFilteredAsync(string? typeFilter, string? osFilter);
     Task<IReadOnlyList<SystemResource>> GetByPhysicalHostAsync(string name);
-}
+}

+ 3 - 4
RackPeek.Domain/Resources/SystemResources/SystemResource.cs

@@ -3,8 +3,7 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.SystemResources;
 
-public class SystemResource : Resource, IDriveResource
-{
+public class SystemResource : Resource, IDriveResource {
     public const string KindLabel = "System";
 
     public static readonly string[] ValidSystemTypes =
@@ -23,7 +22,7 @@ public class SystemResource : Resource, IDriveResource
     public string? Os { get; set; }
     public int? Cores { get; set; }
     public double? Ram { get; set; }
-    public List<Drive>? Drives { get; set; }
-    
+
     public string? Ip { get; set; }
+    public List<Drive>? Drives { get; set; }
 }

+ 2 - 4
RackPeek.Domain/Resources/SystemResources/UseCases/DescribeSystemUseCase.cs

@@ -14,10 +14,8 @@ public record SystemDescription(
     Dictionary<string, string> Labels
 );
 
-public class DescribeSystemUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<SystemDescription> ExecuteAsync(string name)
-    {
+public class DescribeSystemUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<SystemDescription> ExecuteAsync(string name) {
         name = Normalize.SystemName(name);
         ThrowIfInvalid.ResourceName(name);
         var system = await repository.GetByNameAsync(name) as SystemResource;

+ 4 - 7
RackPeek.Domain/Resources/SystemResources/UseCases/GetSystemServiceTreeUseCase.cs

@@ -1,21 +1,18 @@
 using RackPeek.Domain.Helpers;
 using RackPeek.Domain.Persistence;
 using RackPeek.Domain.Resources.Hardware;
-using RackPeek.Domain.Resources.Services;
 
 namespace RackPeek.Domain.Resources.SystemResources.UseCases;
 
 public class GetSystemServiceTreeUseCase(
-    IResourceCollection repo) : IUseCase
-{
-    public async Task<SystemDependencyTree> ExecuteAsync(string name)
-    {
+    IResourceCollection repo) : IUseCase {
+    public async Task<SystemDependencyTree> ExecuteAsync(string name) {
         name = Normalize.SystemName(name);
         ThrowIfInvalid.ResourceName(name);
         var system = await repo.GetByNameAsync(name) as SystemResource;
         if (system is null) throw new NotFoundException($"System '{name}' not found.");
-        var services = await repo.GetDependantsAsync(system.Name);
+        IReadOnlyList<Resource> services = await repo.GetDependantsAsync(system.Name);
 
         return new SystemDependencyTree(system, services.OfType<Resource>());
     }
-}
+}

+ 8 - 12
RackPeek.Domain/Resources/SystemResources/UseCases/GetSystemSummaryUseCase.cs

@@ -1,12 +1,10 @@
 namespace RackPeek.Domain.Resources.SystemResources.UseCases;
 
-public sealed class SystemSummary
-{
+public sealed class SystemSummary {
     public SystemSummary(
         int totalSystems,
         IReadOnlyDictionary<string, int> systemsByType,
-        IReadOnlyDictionary<string, int> systemsByOs)
-    {
+        IReadOnlyDictionary<string, int> systemsByOs) {
         TotalSystems = totalSystems;
         SystemsByType = systemsByType;
         SystemsByOs = systemsByOs;
@@ -17,13 +15,11 @@ public sealed class SystemSummary
     public IReadOnlyDictionary<string, int> SystemsByOs { get; }
 }
 
-public class GetSystemSummaryUseCase(ISystemRepository repository) : IUseCase
-{
-    public async Task<SystemSummary> ExecuteAsync()
-    {
-        var totalSystemsTask = repository.GetSystemCountAsync();
-        var systemsByTypeTask = repository.GetSystemTypeCountAsync();
-        var systemsByOsTask = repository.GetSystemOsCountAsync();
+public class GetSystemSummaryUseCase(ISystemRepository repository) : IUseCase {
+    public async Task<SystemSummary> ExecuteAsync() {
+        Task<int> totalSystemsTask = repository.GetSystemCountAsync();
+        Task<Dictionary<string, int>> systemsByTypeTask = repository.GetSystemTypeCountAsync();
+        Task<Dictionary<string, int>> systemsByOsTask = repository.GetSystemOsCountAsync();
 
         await Task.WhenAll(
             totalSystemsTask,
@@ -37,4 +33,4 @@ public class GetSystemSummaryUseCase(ISystemRepository repository) : IUseCase
             systemsByOsTask.Result
         );
     }
-}
+}

+ 4 - 7
RackPeek.Domain/Resources/SystemResources/UseCases/SystemReportUseCase.cs

@@ -16,14 +16,11 @@ public record SystemReportRow(
     List<string> RunsOn
 );
 
-public class SystemReportUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<SystemReport> ExecuteAsync()
-    {
-        var systems = await repository.GetAllOfTypeAsync<SystemResource>();
+public class SystemReportUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<SystemReport> ExecuteAsync() {
+        IReadOnlyList<SystemResource> systems = await repository.GetAllOfTypeAsync<SystemResource>();
 
-        var rows = systems.Select(system =>
-        {
+        var rows = systems.Select(system => {
             var totalStorage = system.Drives?.Sum(d => d.Size) ?? 0;
 
             return new SystemReportRow(

+ 9 - 21
RackPeek.Domain/Resources/SystemResources/UseCases/UpdateSystemUseCase.cs

@@ -3,8 +3,7 @@ using RackPeek.Domain.Persistence;
 
 namespace RackPeek.Domain.Resources.SystemResources.UseCases;
 
-public class UpdateSystemUseCase(IResourceCollection repository) : IUseCase
-{
+public class UpdateSystemUseCase(IResourceCollection repository) : IUseCase {
     public async Task ExecuteAsync(
         string name,
         string? type = null,
@@ -14,8 +13,7 @@ public class UpdateSystemUseCase(IResourceCollection repository) : IUseCase
         string? ip = null,
         List<string>? runsOn = null,
         string? notes = null
-    )
-    {
+    ) {
         // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
         // ToDo validate / normalize all inputs
 
@@ -27,8 +25,7 @@ public class UpdateSystemUseCase(IResourceCollection repository) : IUseCase
         if (system is null)
             throw new InvalidOperationException($"System '{name}' not found.");
 
-        if (!string.IsNullOrWhiteSpace(type))
-        {
+        if (!string.IsNullOrWhiteSpace(type)) {
             var normalizedSystemType = Normalize.SystemType(type);
             ThrowIfInvalid.SystemType(normalizedSystemType);
             system.Type = normalizedSystemType;
@@ -43,32 +40,23 @@ public class UpdateSystemUseCase(IResourceCollection repository) : IUseCase
         if (ram.HasValue)
             system.Ram = ram.Value;
 
-        if (ip != null)
-        {
-            system.Ip = ip;
-        }
-        
+        if (ip != null) system.Ip = ip;
+
         if (notes != null) system.Notes = notes;
 
         if (runsOn?.Count > 0)
-        {
-            foreach(string parent in runsOn) {
+            foreach (var parent in runsOn)
                 if (!string.IsNullOrWhiteSpace(parent)) {
                     ThrowIfInvalid.ResourceName(parent);
-                    var parentHardware = await repository.GetByNameAsync(parent);
+                    Resource? parentHardware = await repository.GetByNameAsync(parent);
+
 
-                    
                     if (parentHardware == null) throw new NotFoundException($"Parent '{parent}' not found.");
                     if (parentHardware is not Hardware.Hardware and not SystemResource)
-                    {
                         throw new Exception("System cannot run on this resource.");
-                    }
-                    
-                    if (!system.RunsOn.Contains(parent)) system.RunsOn.Add(parent);
 
+                    if (!system.RunsOn.Contains(parent)) system.RunsOn.Add(parent);
                 }
-            }
-        }
 
         await repository.UpdateAsync(system);
     }

+ 3 - 5
RackPeek.Domain/Resources/UpsUnits/DescribeUpsUseCase.cs

@@ -10,10 +10,8 @@ public record UpsDescription(
     Dictionary<string, string> Labels
 );
 
-public class DescribeUpsUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<UpsDescription> ExecuteAsync(string name)
-    {
+public class DescribeUpsUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<UpsDescription> ExecuteAsync(string name) {
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
@@ -28,4 +26,4 @@ public class DescribeUpsUseCase(IResourceCollection repository) : IUseCase
             ups.Labels
         );
     }
-}
+}

+ 3 - 5
RackPeek.Domain/Resources/UpsUnits/UpdateUpsUseCase.cs

@@ -3,15 +3,13 @@ using RackPeek.Domain.Persistence;
 
 namespace RackPeek.Domain.Resources.UpsUnits;
 
-public class UpdateUpsUseCase(IResourceCollection repository) : IUseCase
-{
+public class UpdateUpsUseCase(IResourceCollection repository) : IUseCase {
     public async Task ExecuteAsync(
         string name,
         string? model = null,
         int? va = null,
         string? notes = null
-    )
-    {
+    ) {
         // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
         // ToDo validate / normalize all inputs
 
@@ -30,4 +28,4 @@ public class UpdateUpsUseCase(IResourceCollection repository) : IUseCase
         if (notes != null) ups.Notes = notes;
         await repository.UpdateAsync(ups);
     }
-}
+}

+ 2 - 3
RackPeek.Domain/Resources/UpsUnits/Ups.cs

@@ -1,8 +1,7 @@
 namespace RackPeek.Domain.Resources.UpsUnits;
 
-public class Ups : Hardware.Hardware
-{
+public class Ups : Hardware.Hardware {
     public const string KindLabel = "Ups";
     public string? Model { get; set; }
     public int? Va { get; set; }
-}
+}

+ 5 - 8
RackPeek.Domain/Resources/UpsUnits/UpsHardwareReport.cs

@@ -12,14 +12,11 @@ public record UpsHardwareRow(
     int Va
 );
 
-public class UpsHardwareReportUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<UpsHardwareReport> ExecuteAsync()
-    {
-        var upsUnits = await repository.GetAllOfTypeAsync<Ups>();
+public class UpsHardwareReportUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<UpsHardwareReport> ExecuteAsync() {
+        IReadOnlyList<Ups> upsUnits = await repository.GetAllOfTypeAsync<Ups>();
 
-        var rows = upsUnits.Select(ups =>
-        {
+        var rows = upsUnits.Select(ups => {
             return new UpsHardwareRow(
                 ups.Name,
                 ups.Model ?? "Unknown",
@@ -29,4 +26,4 @@ public class UpsHardwareReportUseCase(IResourceCollection repository) : IUseCase
 
         return new UpsHardwareReport(rows);
     }
-}
+}

+ 2 - 3
RackPeek.Domain/RpkConstants.cs

@@ -1,6 +1,5 @@
 namespace RackPeek.Domain;
 
-public static class RpkConstants
-{
+public static class RpkConstants {
     public const string Version = "v1.2.0";
-}
+}

+ 15 - 19
RackPeek.Domain/ServiceCollectionExtensions.cs

@@ -9,36 +9,32 @@ using RackPeek.Domain.UseCases;
 using RackPeek.Domain.UseCases.Cpus;
 using RackPeek.Domain.UseCases.Drives;
 using RackPeek.Domain.UseCases.Gpus;
-using RackPeek.Domain.UseCases.Nics;
 using RackPeek.Domain.UseCases.Labels;
+using RackPeek.Domain.UseCases.Nics;
 using RackPeek.Domain.UseCases.Ports;
 using RackPeek.Domain.UseCases.Tags;
 
 namespace RackPeek.Domain;
 
-public interface IResourceUseCase<T> where T : Resource
-{
+public interface IResourceUseCase<T> where T : Resource {
 }
 
-public static class ServiceCollectionExtensions
-{
+public static class ServiceCollectionExtensions {
     public static IServiceCollection AddResourceUseCases(
         this IServiceCollection services,
-        Assembly assembly)
-    {
-        var types = assembly.GetTypes()
+        Assembly assembly) {
+        IEnumerable<Type> types = assembly.GetTypes()
             .Where(t => !t.IsAbstract && !t.IsInterface);
 
-        foreach (var type in types)
-        {
-            var resourceUseCaseInterfaces = type.GetInterfaces()
+        foreach (Type type in types) {
+            IEnumerable<Type> resourceUseCaseInterfaces = type.GetInterfaces()
                 .Where(i =>
                     i.IsGenericType &&
                     i.GetInterfaces().Any(parent =>
                         parent.IsGenericType &&
                         parent.GetGenericTypeDefinition() == typeof(IResourceUseCase<>)));
 
-            foreach (var serviceType in resourceUseCaseInterfaces) services.AddScoped(serviceType, type);
+            foreach (Type serviceType in resourceUseCaseInterfaces) services.AddScoped(serviceType, type);
         }
 
         return services;
@@ -46,8 +42,7 @@ public static class ServiceCollectionExtensions
 
 
     public static IServiceCollection AddUseCases(
-        this IServiceCollection services)
-    {
+        this IServiceCollection services) {
         services.AddScoped(typeof(IAddResourceUseCase<>), typeof(AddResourceUseCase<>));
         services.AddScoped(typeof(IAddLabelUseCase<>), typeof(AddLabelUseCase<>));
         services.AddScoped(typeof(IAddTagUseCase<>), typeof(AddTagUseCase<>));
@@ -80,24 +75,25 @@ public static class ServiceCollectionExtensions
         services.AddScoped(typeof(IRemoveNicUseCase<>), typeof(RemoveNicUseCase<>));
         services.AddScoped(typeof(IUpdateNicUseCase<>), typeof(UpdateNicUseCase<>));
 
-        var usecases = Assembly.GetAssembly(typeof(IUseCase))
+        IEnumerable<Type>? usecases = Assembly.GetAssembly(typeof(IUseCase))
             ?.GetTypes()
             .Where(t =>
                 !t.IsAbstract &&
                 typeof(IUseCase).IsAssignableFrom(t)
             );
 
-        foreach (var type in usecases) services.AddScoped(type);
+        if (usecases != null)
+            foreach (Type type in usecases)
+                services.AddScoped(type);
 
         return services;
     }
 
     public static IServiceCollection AddYamlRepos(
-        this IServiceCollection services)
-    {
+        this IServiceCollection services) {
         services.AddScoped<IHardwareRepository, YamlHardwareRepository>();
         services.AddScoped<ISystemRepository, YamlSystemRepository>();
         services.AddScoped<IServiceRepository, ServiceRepository>();
         return services;
     }
-}
+}

+ 6 - 11
RackPeek.Domain/UseCases/AddResourceUseCase.cs

@@ -5,37 +5,32 @@ using RackPeek.Domain.Resources;
 namespace RackPeek.Domain.UseCases;
 
 public interface IAddResourceUseCase<T> : IResourceUseCase<T>
-    where T : Resource
-{
+    where T : Resource {
     Task ExecuteAsync(string name, List<string>? runsOn = null);
 }
 
-public class AddResourceUseCase<T>(IResourceCollection repo) : IAddResourceUseCase<T> where T : Resource
-{
-    public async Task ExecuteAsync(string name, List<string>? runsOn = null)
-    {
+public class AddResourceUseCase<T>(IResourceCollection repo) : IAddResourceUseCase<T> where T : Resource {
+    public async Task ExecuteAsync(string name, List<string>? runsOn = null) {
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
-        var existingResource = await repo.GetByNameAsync(name);
+        Resource? existingResource = await repo.GetByNameAsync(name);
         if (existingResource != null)
             throw new ConflictException($"Resource '{name}' ({existingResource.Kind}) already exists.");
 
         if (runsOn != null)
-        {
             foreach (var parent in runsOn) {
                 var normalizedParent = Normalize.HardwareName(parent);
                 ThrowIfInvalid.ResourceName(normalizedParent);
-                var parentResource = await repo.GetByNameAsync(normalizedParent);
+                Resource? parentResource = await repo.GetByNameAsync(normalizedParent);
                 if (parentResource == null) throw new NotFoundException($"Resource '{normalizedParent}' not found.");
 
                 if (!Resource.CanRunOn<T>(parentResource))
                     throw new InvalidOperationException(
                         $" {Resource.GetKind<T>()} cannot run on {parentResource.Kind} '{normalizedParent}'.");
             }
-        }
 
-        var resource = Activator.CreateInstance<T>();
+        T resource = Activator.CreateInstance<T>();
         resource.Name = name;
         resource.RunsOn = runsOn ?? new List<string>();
 

+ 49 - 86
RackPeek.Domain/UseCases/Ansible/AnsibleInventoryGenerator.cs

@@ -3,33 +3,31 @@ using RackPeek.Domain.Resources;
 
 namespace RackPeek.Domain.UseCases.Ansible;
 
-public enum InventoryFormat
-{
+public enum InventoryFormat {
     Ini,
     Yaml
 }
 
-public sealed record InventoryOptions
-{
+public sealed record InventoryOptions {
     /// <summary>
-    /// Output format (default: INI)
+    ///     Output format (default: INI)
     /// </summary>
     public InventoryFormat Format { get; init; } = InventoryFormat.Ini;
 
     /// <summary>
-    /// If set, create groups based on these tags.
-    /// Example: ["prod", "staging"] -> [prod], [staging]
+    ///     If set, create groups based on these tags.
+    ///     Example: ["prod", "staging"] -> [prod], [staging]
     /// </summary>
     public IReadOnlyList<string> GroupByTags { get; init; } = [];
 
     /// <summary>
-    /// If set, create groups based on these label keys.
-    /// Example: ["env"] -> [env_prod]
+    ///     If set, create groups based on these label keys.
+    ///     Example: ["env"] -> [env_prod]
     /// </summary>
     public IReadOnlyList<string> GroupByLabelKeys { get; init; } = [];
 
     /// <summary>
-    /// If set, emitted under [all:vars] (INI) or all.vars (YAML).
+    ///     If set, emitted under [all:vars] (INI) or all.vars (YAML).
     /// </summary>
     public IDictionary<string, string> GlobalVars { get; init; } =
         new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@@ -37,69 +35,53 @@ public sealed record InventoryOptions
 
 public sealed record InventoryResult(string InventoryText, IReadOnlyList<string> Warnings);
 
-public static class AnsibleInventoryGenerator
-{
+public static class AnsibleInventoryGenerator {
     public static InventoryResult ToAnsibleInventory(
         this IReadOnlyList<Resource> resources,
-        InventoryOptions? options = null)
-    {
+        InventoryOptions? options = null) {
         options ??= new InventoryOptions();
 
-        var model = BuildInventoryModel(resources, options);
+        InventoryModel model = BuildInventoryModel(resources, options);
 
-        return options.Format switch
-        {
+        return options.Format switch {
             InventoryFormat.Yaml => RenderYaml(model, options),
             _ => RenderIni(model, options)
         };
     }
 
-    private sealed record HostEntry(
-        string Name,
-        Dictionary<string, string> Vars,
-        Resource Resource);
-
-    private sealed record InventoryModel(
-        Dictionary<string, List<HostEntry>> Groups,
-        List<string> Warnings);
-
     private static InventoryModel BuildInventoryModel(
         IReadOnlyList<Resource> resources,
-        InventoryOptions options)
-    {
+        InventoryOptions options) {
         var warnings = new List<string>();
         var hosts = new List<HostEntry>();
 
-        foreach (var r in resources)
-        {
+        foreach (Resource r in resources) {
             var address = GetAddress(r);
 
             if (string.IsNullOrWhiteSpace(address))
                 continue;
 
-            var vars = BuildHostVars(r, address);
+            Dictionary<string, string> vars = BuildHostVars(r, address);
             hosts.Add(new HostEntry(r.Name, vars, r));
         }
 
         var groupToHosts =
             new Dictionary<string, List<HostEntry>>(StringComparer.OrdinalIgnoreCase);
 
-        void AddToGroup(string groupName, HostEntry h)
-        {
+        void AddToGroup(string groupName, HostEntry h) {
             if (string.IsNullOrWhiteSpace(groupName))
                 return;
 
             groupName = SanitizeGroup(groupName);
 
-            if (!groupToHosts.TryGetValue(groupName, out var list))
+            if (!groupToHosts.TryGetValue(groupName, out List<HostEntry>? list))
                 groupToHosts[groupName] = list = new List<HostEntry>();
 
             if (!list.Any(x => string.Equals(x.Name, h.Name, StringComparison.OrdinalIgnoreCase)))
                 list.Add(h);
         }
 
-        foreach (var h in hosts)
-        {
+        foreach (HostEntry h in hosts) {
             // Tag-based groups
             var matchingTags = options.GroupByTags
                 .Intersect(h.Resource.Tags ?? [])
@@ -110,52 +92,40 @@ public static class AnsibleInventoryGenerator
 
             // Label-based groups
             foreach (var key in options.GroupByLabelKeys)
-            {
                 if (h.Resource.Labels.TryGetValue(key, out var val)
                     && !string.IsNullOrWhiteSpace(val))
-                {
                     AddToGroup($"{key}_{val}", h);
-                }
-            }
         }
 
         return new InventoryModel(groupToHosts, warnings);
     }
-    
+
     private static InventoryResult RenderIni(
         InventoryModel model,
-        InventoryOptions options)
-    {
+        InventoryOptions options) {
         var sb = new StringBuilder();
 
-        if (options.GlobalVars.Count > 0)
-        {
+        if (options.GlobalVars.Count > 0) {
             sb.AppendLine("[all:vars]");
 
-            foreach (var kvp in options.GlobalVars
+            foreach (KeyValuePair<string, string> kvp in options.GlobalVars
                          .OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase))
-            {
                 sb.AppendLine($"{kvp.Key}={EscapeIniValue(kvp.Value)}");
-            }
 
             sb.AppendLine();
         }
 
         foreach (var group in model.Groups.Keys
-                     .OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
-        {
+                     .OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) {
             sb.AppendLine($"[{group}]");
 
-            foreach (var host in model.Groups[group]
-                         .OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase))
-            {
+            foreach (HostEntry host in model.Groups[group]
+                         .OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)) {
                 sb.Append(host.Name);
 
-                foreach (var kvp in host.Vars
+                foreach (KeyValuePair<string, string> kvp in host.Vars
                              .OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase))
-                {
                     sb.Append($" {kvp.Key}={EscapeIniValue(kvp.Value)}");
-                }
 
                 sb.AppendLine();
             }
@@ -168,48 +138,39 @@ public static class AnsibleInventoryGenerator
 
     private static InventoryResult RenderYaml(
         InventoryModel model,
-        InventoryOptions options)
-    {
+        InventoryOptions options) {
         var sb = new StringBuilder();
 
         sb.AppendLine("all:");
 
-        if (options.GlobalVars.Count > 0)
-        {
+        if (options.GlobalVars.Count > 0) {
             sb.AppendLine("  vars:");
-            foreach (var kvp in options.GlobalVars
+            foreach (KeyValuePair<string, string> kvp in options.GlobalVars
                          .OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase))
-            {
                 sb.AppendLine($"    {kvp.Key}: {kvp.Value}");
-            }
         }
 
         sb.AppendLine("  children:");
 
         foreach (var group in model.Groups.Keys
-                     .OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
-        {
+                     .OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) {
             sb.AppendLine($"    {group}:");
             sb.AppendLine("      hosts:");
 
-            foreach (var host in model.Groups[group]
-                         .OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase))
-            {
+            foreach (HostEntry host in model.Groups[group]
+                         .OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)) {
                 sb.AppendLine($"        {host.Name}:");
 
-                foreach (var kvp in host.Vars
+                foreach (KeyValuePair<string, string> kvp in host.Vars
                              .OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase))
-                {
                     sb.AppendLine($"          {kvp.Key}: {kvp.Value}");
-                }
             }
         }
 
         return new InventoryResult(sb.ToString().TrimEnd(), model.Warnings);
     }
 
-    private static string? GetAddress(Resource r)
-    {
+    private static string? GetAddress(Resource r) {
         if (r.Labels.TryGetValue("ansible_host", out var ah) && !string.IsNullOrWhiteSpace(ah))
             return ah;
 
@@ -222,15 +183,12 @@ public static class AnsibleInventoryGenerator
         return null;
     }
 
-    private static Dictionary<string, string> BuildHostVars(Resource r, string address)
-    {
-        var vars = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
-        {
+    private static Dictionary<string, string> BuildHostVars(Resource r, string address) {
+        var vars = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {
             ["ansible_host"] = address
         };
 
-        foreach (var (k, v) in r.Labels)
-        {
+        foreach ((var k, var v) in r.Labels) {
             if (string.IsNullOrWhiteSpace(k) || string.IsNullOrWhiteSpace(v))
                 continue;
 
@@ -241,24 +199,20 @@ public static class AnsibleInventoryGenerator
         return vars;
     }
 
-    private static string SanitizeGroup(string s)
-    {
+    private static string SanitizeGroup(string s) {
         var sb = new StringBuilder();
 
         foreach (var ch in s.Trim().ToLowerInvariant())
-        {
             if (char.IsLetterOrDigit(ch) || ch == '_')
                 sb.Append(ch);
             else if (ch == '-' || ch == '.' || ch == ' ')
                 sb.Append('_');
-        }
 
         var result = sb.ToString();
         return string.IsNullOrWhiteSpace(result) ? "group" : result;
     }
 
-    private static string EscapeIniValue(string value)
-    {
+    private static string EscapeIniValue(string value) {
         if (string.IsNullOrEmpty(value))
             return "\"\"";
 
@@ -270,4 +224,13 @@ public static class AnsibleInventoryGenerator
 
         return "\"" + value.Replace("\"", "\\\"") + "\"";
     }
-}
+
+    private sealed record HostEntry(
+        string Name,
+        Dictionary<string, string> Vars,
+        Resource Resource);
+
+    private sealed record InventoryModel(
+        Dictionary<string, List<HostEntry>> Groups,
+        List<string> Warnings);
+}

+ 4 - 6
RackPeek.Domain/UseCases/Ansible/AnsibleInventoryGeneratorUseCase.cs

@@ -3,11 +3,9 @@ using RackPeek.Domain.Resources;
 
 namespace RackPeek.Domain.UseCases.Ansible;
 
-public class AnsibleInventoryGeneratorUseCase(IResourceCollection repository) : IUseCase
-{
-    public async Task<InventoryResult?> ExecuteAsync(InventoryOptions options)
-    {
-        var resources = await repository.GetAllOfTypeAsync<Resource>();
+public class AnsibleInventoryGeneratorUseCase(IResourceCollection repository) : IUseCase {
+    public async Task<InventoryResult?> ExecuteAsync(InventoryOptions options) {
+        IReadOnlyList<Resource> resources = await repository.GetAllOfTypeAsync<Resource>();
         return resources.ToAnsibleInventory(options);
     }
-}
+}

+ 6 - 9
RackPeek.Domain/UseCases/CloneAccessPointUseCase.cs

@@ -5,31 +5,28 @@ using RackPeek.Domain.Resources;
 namespace RackPeek.Domain.UseCases;
 
 public interface ICloneResourceUseCase<T> : IResourceUseCase<T>
-    where T : Resource
-{
+    where T : Resource {
     public Task ExecuteAsync(string originalName, string cloneName);
 }
 
-public class CloneResourceUseCase<T>(IResourceCollection repo) : ICloneResourceUseCase<T> where T : Resource
-{
-    public async Task ExecuteAsync(string originalName, string cloneName)
-    {
+public class CloneResourceUseCase<T>(IResourceCollection repo) : ICloneResourceUseCase<T> where T : Resource {
+    public async Task ExecuteAsync(string originalName, string cloneName) {
         originalName = Normalize.HardwareName(originalName);
         ThrowIfInvalid.ResourceName(originalName);
 
         cloneName = Normalize.HardwareName(cloneName);
         ThrowIfInvalid.ResourceName(cloneName);
 
-        var resource = await repo.GetByNameAsync(cloneName);
+        Resource? resource = await repo.GetByNameAsync(cloneName);
         if (resource != null)
             throw new ConflictException($"{resource.Kind} resource '{cloneName}' already exists.");
 
         var original = await repo.GetByNameAsync(originalName) as T;
         if (original == null) throw new NotFoundException($"Resource '{originalName}' not found.");
 
-        var clone = Clone.DeepClone(original);
+        T clone = Clone.DeepClone(original);
         clone.Name = cloneName;
 
         await repo.AddAsync(clone);
     }
-}
+}

+ 7 - 11
RackPeek.Domain/UseCases/Cpus/AddCpuUseCase.cs

@@ -7,8 +7,7 @@ using RackPeek.Domain.Resources.SubResources;
 namespace RackPeek.Domain.UseCases.Cpus;
 
 public interface IAddCpuUseCase<T> : IResourceUseCase<T>
-    where T : Resource
-{
+    where T : Resource {
     public Task ExecuteAsync(
         string name,
         string? model,
@@ -16,29 +15,26 @@ public interface IAddCpuUseCase<T> : IResourceUseCase<T>
         int? threads);
 }
 
-public class AddCpuUseCase<T>(IResourceCollection repo) : IAddCpuUseCase<T> where T : Resource
-{
+public class AddCpuUseCase<T>(IResourceCollection repo) : IAddCpuUseCase<T> where T : Resource {
     public async Task ExecuteAsync(
         string name,
         string? model,
         int? cores,
-        int? threads)
-    {
+        int? threads) {
         // ToDo pass in properties as inputs, construct the entity in the usecase
         // ToDo validate / normalize all inputs
 
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
-        var resource = await repo.GetByNameAsync<T>(name) ??
-                       throw new NotFoundException($"Resource '{name}' not found.");
+        T resource = await repo.GetByNameAsync<T>(name) ??
+                     throw new NotFoundException($"Resource '{name}' not found.");
 
         if (resource is not ICpuResource cpuResource) return;
 
         cpuResource.Cpus ??= [];
 
-        cpuResource.Cpus.Add(new Cpu
-        {
+        cpuResource.Cpus.Add(new Cpu {
             Model = model,
             Cores = cores,
             Threads = threads
@@ -46,4 +42,4 @@ public class AddCpuUseCase<T>(IResourceCollection repo) : IAddCpuUseCase<T> wher
 
         await repo.UpdateAsync(resource);
     }
-}
+}

+ 6 - 9
RackPeek.Domain/UseCases/Cpus/RemoveCpuUseCase.cs

@@ -6,24 +6,21 @@ using RackPeek.Domain.Resources.Servers;
 namespace RackPeek.Domain.UseCases.Cpus;
 
 public interface IRemoveCpuUseCase<T> : IResourceUseCase<T>
-    where T : Resource
-{
+    where T : Resource {
     public Task ExecuteAsync(
         string name,
         int index);
 }
 
-public class RemoveCpuUseCase<T>(IResourceCollection repo) : IRemoveCpuUseCase<T> where T : Resource
-{
+public class RemoveCpuUseCase<T>(IResourceCollection repo) : IRemoveCpuUseCase<T> where T : Resource {
     public async Task ExecuteAsync(
         string name,
-        int index)
-    {
+        int index) {
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
-        var resource = await repo.GetByNameAsync<T>(name) ??
-                       throw new NotFoundException($"Resource '{name}' not found.");
+        T resource = await repo.GetByNameAsync<T>(name) ??
+                     throw new NotFoundException($"Resource '{name}' not found.");
         if (resource is not ICpuResource cpuResource) return;
 
         cpuResource.Cpus ??= [];
@@ -41,4 +38,4 @@ public class RemoveCpuUseCase<T>(IResourceCollection repo) : IRemoveCpuUseCase<T
 
         await repo.UpdateAsync(resource);
     }
-}
+}

+ 8 - 10
RackPeek.Domain/UseCases/Cpus/UpdateCpuUseCase.cs

@@ -2,12 +2,12 @@ using RackPeek.Domain.Helpers;
 using RackPeek.Domain.Persistence;
 using RackPeek.Domain.Resources;
 using RackPeek.Domain.Resources.Servers;
+using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.UseCases.Cpus;
 
 public interface IUpdateCpuUseCase<T> : IResourceUseCase<T>
-    where T : Resource
-{
+    where T : Resource {
     public Task ExecuteAsync(
         string name,
         int index,
@@ -16,23 +16,21 @@ public interface IUpdateCpuUseCase<T> : IResourceUseCase<T>
         int? threads);
 }
 
-public class UpdateCpuUseCase<T>(IResourceCollection repo) : IUpdateCpuUseCase<T> where T : Resource
-{
+public class UpdateCpuUseCase<T>(IResourceCollection repo) : IUpdateCpuUseCase<T> where T : Resource {
     public async Task ExecuteAsync(
         string name,
         int index,
         string? model,
         int? cores,
-        int? threads)
-    {
+        int? threads) {
         // ToDo pass in properties as inputs, construct the entity in the usecase
         // ToDo validate / normalize all inputs
 
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
-        var resource = await repo.GetByNameAsync<T>(name) ??
-                       throw new NotFoundException($"Resource '{name}' not found.");
+        T resource = await repo.GetByNameAsync<T>(name) ??
+                     throw new NotFoundException($"Resource '{name}' not found.");
 
         if (resource is not ICpuResource cpuResource) return;
 
@@ -48,11 +46,11 @@ public class UpdateCpuUseCase<T>(IResourceCollection repo) : IUpdateCpuUseCase<T
         if (index >= cpuResource.Cpus.Count)
             throw new NotFoundException($"Please pick a CPU index < {cpuResource.Cpus.Count} for '{name}'.");
 
-        var cpu = cpuResource.Cpus[index];
+        Cpu cpu = cpuResource.Cpus[index];
         cpu.Model = model;
         cpu.Cores = cores;
         cpu.Threads = threads;
 
         await repo.UpdateAsync(resource);
     }
-}
+}

+ 8 - 12
RackPeek.Domain/UseCases/DeleteResourceUseCase.cs

@@ -5,29 +5,25 @@ using RackPeek.Domain.Resources;
 namespace RackPeek.Domain.UseCases;
 
 public interface IDeleteResourceUseCase<T> : IResourceUseCase<T>
-    where T : Resource
-{
+    where T : Resource {
     Task ExecuteAsync(string name);
 }
 
-public class DeleteResourceUseCase<T>(IResourceCollection repo) : IDeleteResourceUseCase<T> where T : Resource
-{
-    public async Task ExecuteAsync(string name)
-    {
+public class DeleteResourceUseCase<T>(IResourceCollection repo) : IDeleteResourceUseCase<T> where T : Resource {
+    public async Task ExecuteAsync(string name) {
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
-        var existingResource = await repo.GetByNameAsync(name);
+        Resource? existingResource = await repo.GetByNameAsync(name);
         if (existingResource == null)
             throw new NotFoundException($"Resource '{name}' does not exist.");
 
-        var dependants = await repo.GetDependantsAsync(name);
-        foreach (var resource in dependants)
-        {
-            resource.RunsOn = null;
+        IReadOnlyList<Resource> dependants = await repo.GetDependantsAsync(name);
+        foreach (Resource resource in dependants) {
+            resource.RunsOn.Remove(name);
             await repo.UpdateAsync(resource);
         }
 
         await repo.DeleteAsync(name);
     }
-}
+}

+ 7 - 11
RackPeek.Domain/UseCases/Drives/AddDriveUseCase.cs

@@ -7,39 +7,35 @@ using RackPeek.Domain.Resources.SubResources;
 namespace RackPeek.Domain.UseCases.Drives;
 
 public interface IAddDriveUseCase<T> : IResourceUseCase<T>
-    where T : Resource
-{
+    where T : Resource {
     public Task ExecuteAsync(
         string name,
         string? type,
         int? size);
 }
 
-public class AddDriveUseCase<T>(IResourceCollection repository) : IAddDriveUseCase<T> where T : Resource
-{
+public class AddDriveUseCase<T>(IResourceCollection repository) : IAddDriveUseCase<T> where T : Resource {
     public async Task ExecuteAsync(
         string name,
         string? type,
-        int? size)
-    {
+        int? size) {
         // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
         // ToDo validate / normalize all inputs
 
         name = Normalize.HardwareName(name);
         ThrowIfInvalid.ResourceName(name);
 
-        var resource = await repository.GetByNameAsync<T>(name) ??
-                       throw new NotFoundException($"Resource '{name}' not found.");
+        T resource = await repository.GetByNameAsync<T>(name) ??
+                     throw new NotFoundException($"Resource '{name}' not found.");
 
         if (resource is not IDriveResource dr) throw new NotFoundException($"Resource '{name}' not found.");
 
         dr.Drives ??= new List<Drive>();
-        dr.Drives.Add(new Drive
-        {
+        dr.Drives.Add(new Drive {
             Type = type,
             Size = size
         });
 
         await repository.UpdateAsync(resource);
     }
-}
+}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно