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

Fix ResourceCollectionMerger for Other; audit consistency across features and docs

- Register Other in the merger's polymorphic JSON resolver: without
  it, any YAML merge or /api/inventory upsert touching a config that
  contains an Other resource threw (FailSerialization). Regression
  test added (verified failing before the fix).
- Give Other an explicit Mermaid shape (plain rect) in topology
  diagrams instead of relying on the fallback.
- Document the Other kind in resource-levels.md, inventory-api.md,
  and AGENTS.md kind lists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tim Jones 11 часов назад
Родитель
Сommit
9316f05bd5

+ 5 - 5
AGENTS.md

@@ -6,7 +6,7 @@ This document is the entry point for AI agents (Claude Code, OpenCode, etc.) wor
 
 ## 1. What RackPeek is
 
-RackPeek is a **CLI + Web UI for documenting and managing home-lab / small-scale IT infrastructure** (servers, switches, routers, firewalls, access points, UPS units, desktops, laptops, systems, and services).
+RackPeek is a **CLI + Web UI for documenting and managing home-lab / small-scale IT infrastructure** (servers, switches, routers, firewalls, access points, UPS units, desktops, laptops, other hardware, systems, and services).
 
 - All state is persisted to a **single YAML file** (`config/config.yaml`) — no database.
 - Same domain code powers the CLI binary (`rpk`) and the Blazor Server Web UI.
@@ -170,7 +170,7 @@ Top-level shape:
 
 ```yaml
 resources:
-  - kind: Server | Switch | Firewall | Router | Accesspoint | Desktop | Laptop | Ups | System | Service
+  - kind: Server | Switch | Firewall | Router | Accesspoint | Desktop | Laptop | Ups | Other | System | Service
     name: <unique name within kind>
     tags: [...]
     labels: { key: value }
@@ -185,8 +185,8 @@ Key invariants (see `RackPeek.Domain/Resources/Resource.cs`):
 - `name` is the identity within a `kind`. Don't introduce numeric IDs.
 - `runsOn` relationships are validated by `Resource.CanRunOn<T>`:
   - `Service` may run on a `System`.
-  - `System` may run on hardware (`Server`, `Switch`, `Firewall`, `Router`, `Accesspoint`, `Desktop`, `Laptop`, `Ups`) or on another `System`.
-- "Hardware" is the umbrella term for the eight physical kinds above (`Resource.IsHardware`).
+  - `System` may run on hardware (`Server`, `Switch`, `Firewall`, `Router`, `Accesspoint`, `Desktop`, `Laptop`, `Ups`, `Other`) or on another `System`.
+- "Hardware" is the umbrella term for the nine physical kinds above (`Resource.IsHardware`).
 - Anything that mutates the YAML must go through an `IResourceUseCase<T>` → `IResourceCollection` → repository, never direct file writes.
 
 ### YAML migrations
@@ -213,7 +213,7 @@ The full command tree is documented in `docs/Commands.md` and `docs/CommandIndex
 rpk <kind> <verb> [name] [flags]
 
 kinds:   summary, servers, switches, routers, firewalls, systems,
-         accesspoints, ups, desktops, laptops, services
+         accesspoints, ups, desktops, laptops, other, services
 verbs:   summary, add, list, get, describe, set, del, tree
 sub:     cpu, drive, gpu, nic, port, subnets, labels, tags, rename, …
 ```

+ 1 - 0
RackPeek.Domain/Graph/Serialisers/MermaidSerialiser.cs

@@ -43,6 +43,7 @@ public sealed class MermaidSerialiser {
             ["Ups"] = new("{\"", "\"}"),           // rhombus — utility
             ["Desktop"] = new("(\"", "\")"),       // rounded rect — endpoint
             ["Laptop"] = new("(\"", "\")"),        // rounded rect — endpoint
+            ["Other"] = new("[\"", "\"]"),         // plain rect — uncategorised
 
             // Logical / service view shapes (don't appear with the physical
             // kinds in the same diagram, so shape reuse across views is OK)

+ 4 - 0
RackPeek.Domain/Persistence/ResourceCollectionMerger.cs

@@ -12,6 +12,7 @@ using RackPeek.Domain.Resources.Routers;
 using RackPeek.Domain.Resources.Servers;
 using RackPeek.Domain.Resources.Services;
 using RackPeek.Domain.Resources.Switches;
+using RackPeek.Domain.Resources.OtherHardware;
 using RackPeek.Domain.Resources.SystemResources;
 using RackPeek.Domain.Resources.UpsUnits;
 
@@ -185,6 +186,9 @@ internal static class ResourcePolymorphismResolver {
                 typeInfo.PolymorphismOptions.DerivedTypes.Add(
                     new JsonDerivedType(typeof(Ups), Ups.KindLabel));
 
+                typeInfo.PolymorphismOptions.DerivedTypes.Add(
+                    new JsonDerivedType(typeof(Other), Other.KindLabel));
+
                 typeInfo.PolymorphismOptions.DerivedTypes.Add(
                     new JsonDerivedType(typeof(SystemResource), SystemResource.KindLabel));
 

+ 1 - 1
Shared.Rcl/wwwroot/raw_docs/inventory-api.md

@@ -118,7 +118,7 @@ in the repository.
 
 The `kind` discriminator is case-sensitive. Use the exact value for each
 resource type: `Server`, `Switch`, `Router`, `Firewall`, `AccessPoint`,
-`Ups`, `Desktop`, `Laptop`, `Service`, `System`.
+`Ups`, `Desktop`, `Laptop`, `Other`, `Service`, `System`.
 
 #### Response
 

+ 1 - 0
Shared.Rcl/wwwroot/raw_docs/resource-levels.md

@@ -42,6 +42,7 @@ network switches, Wi-Fi access points, UPS units, and workstations.
 | **Desktop**      | Desktop workstation          | Custom build, Mac Mini                   |
 | **Laptop**       | Laptop                       | ThinkPad T14s                            |
 | **UPS**          | Uninterruptible power supply | APC SMT1500                              |
+| **Other**        | Anything that doesn't fit an existing category | Microwave radio bridge, KVM switch |
 
 ### Sub-Resources
 

+ 45 - 0
Tests/Api/InventoryEndpointTests.cs

@@ -848,4 +848,49 @@ public class InventoryEndpointTests(ITestOutputHelper output) : ApiTestBase(outp
         Assert.Contains("team: backend", newYaml);
         Assert.DoesNotContain("env: production", newYaml);
     }
+
+    [Fact]
+    public async Task Merge_Other_Hardware_Persists() {
+        HttpClient client = CreateClient(true);
+
+        var yaml = """
+                   resources:
+                     - kind: Other
+                       name: radio-merge
+                       model: Building Bridge XG
+                       description: Microwave radio bridge
+                   """;
+
+        HttpResponseMessage response = await client.PostAsJsonAsync("/api/inventory",
+            new {
+                Yaml = yaml,
+                mode = "Merge"
+            });
+
+        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+        ImportYamlResponse? result = await response.Content.ReadFromJsonAsync<ImportYamlResponse>();
+
+        Assert.Contains("radio-merge", result!.Added);
+
+        var update = """
+                     resources:
+                       - kind: Other
+                         name: radio-merge
+                         description: Site-to-site connectivity
+                     """;
+
+        HttpResponseMessage response2 = await client.PostAsJsonAsync("/api/inventory",
+            new { yaml = update, mode = "Merge" });
+
+        Assert.Equal(HttpStatusCode.OK, response2.StatusCode);
+
+        ImportYamlResponse? result2 = await response2.Content.ReadFromJsonAsync<ImportYamlResponse>();
+
+        Assert.Contains("radio-merge", result2!.Updated);
+
+        var newYaml = result2.NewYaml["radio-merge"];
+        Assert.Contains("model: Building Bridge XG", newYaml);
+        Assert.Contains("description: Site-to-site connectivity", newYaml);
+    }
 }