Преглед изворни кода

Merge pull request #39 from Timmoth/Create-WebUI

WebUI project
Tim Jones пре 2 месеци
родитељ
комит
b9952032d1
37 измењених фајлова са 2058 додато и 0 уклоњено
  1. BIN
      .DS_Store
  2. 1 0
      .idea/.idea.RackPeek/.idea/.name
  3. 14 0
      RackPeek.Domain/Resources/Hardware/IHardwareRepository.cs
  4. BIN
      RackPeek.Web/.DS_Store
  5. 20 0
      RackPeek.Web/Components/App.razor
  6. 83 0
      RackPeek.Web/Components/Components/HardwareTreeComponent.razor
  7. 115 0
      RackPeek.Web/Components/Components/ServersListComponent.razor
  8. 3 0
      RackPeek.Web/Components/Layout/MainLayout.razor
  9. 20 0
      RackPeek.Web/Components/Layout/MainLayout.razor.css
  10. 31 0
      RackPeek.Web/Components/Layout/ReconnectModal.razor
  11. 157 0
      RackPeek.Web/Components/Layout/ReconnectModal.razor.css
  12. 63 0
      RackPeek.Web/Components/Layout/ReconnectModal.razor.js
  13. 6 0
      RackPeek.Web/Components/Pages/Error.razor
  14. 6 0
      RackPeek.Web/Components/Pages/Home.razor
  15. 5 0
      RackPeek.Web/Components/Pages/NotFound.razor
  16. 6 0
      RackPeek.Web/Components/Routes.razor
  17. 11 0
      RackPeek.Web/Components/_Imports.razor
  18. 78 0
      RackPeek.Web/Program.cs
  19. 23 0
      RackPeek.Web/Properties/launchSettings.json
  20. 14 0
      RackPeek.Web/RackPeek.Web.csproj
  21. 8 0
      RackPeek.Web/appsettings.Development.json
  22. 9 0
      RackPeek.Web/appsettings.json
  23. 585 0
      RackPeek.Web/config/Services.yaml
  24. 184 0
      RackPeek.Web/config/Systems.yaml
  25. 31 0
      RackPeek.Web/config/accesspoints.yaml
  26. 21 0
      RackPeek.Web/config/desktops.yaml
  27. 14 0
      RackPeek.Web/config/firewalls.yaml
  28. 17 0
      RackPeek.Web/config/laptops.yaml
  29. 14 0
      RackPeek.Web/config/routers.yaml
  30. 397 0
      RackPeek.Web/config/servers.yaml
  31. 14 0
      RackPeek.Web/config/switches.yaml
  32. 6 0
      RackPeek.Web/config/ups.yaml
  33. 38 0
      RackPeek.Web/wwwroot/app.css
  34. 6 0
      RackPeek.Web/wwwroot/tailwind.js
  35. 6 0
      RackPeek.sln
  36. BIN
      RackPeek/.DS_Store
  37. 52 0
      RackPeek/Yaml/YamlHardwareRepository.cs

+ 1 - 0
.idea/.idea.RackPeek/.idea/.name

@@ -0,0 +1 @@
+RackPeek

+ 14 - 0
RackPeek.Domain/Resources/Hardware/IHardwareRepository.cs

@@ -7,4 +7,18 @@ public interface IHardwareRepository
     Task UpdateAsync(Models.Hardware hardware);
     Task DeleteAsync(string name);
     Task<Models.Hardware?> GetByNameAsync(string name);
+    public Task<List<HardwareTree>> GetTreeAsync();
+}
+
+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 required string SystemName { get; set; }
+    public required List<string> Services {get; set;}
 }

BIN
RackPeek.Web/.DS_Store


+ 20 - 0
RackPeek.Web/Components/App.razor

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+    <base href="/"/>
+    <ResourcePreloader/>
+    <ImportMap/>
+    <HeadOutlet @rendermode="InteractiveServer"/>
+    <script src="tailwind.js"></script>
+</head>
+
+<body class="bg-black">
+<Routes @rendermode="InteractiveServer"/>
+<ReconnectModal/>
+<script src="@Assets["_framework/blazor.web.js"]"></script>
+</body>
+
+</html>

+ 83 - 0
RackPeek.Web/Components/Components/HardwareTreeComponent.razor

@@ -0,0 +1,83 @@
+@page "/tree"
+@using RackPeek.Domain.Resources.Hardware
+@inject IHardwareRepository HardwareRepository
+
+<PageTitle>Tree View</PageTitle>
+
+<div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6">
+    <h1 class="text-lg text-zinc-100 mb-6">
+        Tree View
+    </h1>
+
+    @if (_tree is null)
+    {
+        <div class="text-zinc-500">loading tree…</div>
+    }
+    else if (_tree.Count == 0)
+    {
+        <div class="text-zinc-500">no resources found</div>
+    }
+    else
+    {
+        @foreach (var group in _tree
+            .OrderBy(h => h.Kind)
+            .ThenBy(h => h.HardwareName)
+            .GroupBy(h => h.Kind))
+        {
+            <!-- Hardware Kind Header -->
+            <div class="mb-6">
+                <div class="text-xs text-zinc-500 uppercase tracking-wider mb-3">
+                    @group.Key
+                </div>
+
+                <ul class="space-y-4">
+                    @foreach (var hardware in group)
+                    {
+                        <li>
+                            <!-- Hardware -->
+                            <div class="text-zinc-100">
+                                @hardware.HardwareName
+                            </div>
+
+                            @if (hardware.Systems.Any())
+                            {
+                                <ul class="ml-4 mt-2 border-l border-zinc-800 pl-4 space-y-2">
+                                    @foreach (var system in hardware.Systems.OrderBy(s => s.SystemName))
+                                    {
+                                        <li>
+                                            <!-- System -->
+                                            <div class="text-zinc-300">
+                                                └─ @system.SystemName
+                                            </div>
+
+                                            @if (system.Services.Any())
+                                            {
+                                                <ul class="ml-4 mt-1 space-y-1">
+                                                    @foreach (var service in system.Services.OrderBy(s => s))
+                                                    {
+                                                        <li class="text-zinc-500">
+                                                            > @service
+                                                        </li>
+                                                    }
+                                                </ul>
+                                            }
+                                        </li>
+                                    }
+                                </ul>
+                            }
+                        </li>
+                    }
+                </ul>
+            </div>
+        }
+    }
+</div>
+
+@code {
+    private List<HardwareTree>? _tree;
+
+    protected override async Task OnInitializedAsync()
+    {
+        _tree = await HardwareRepository.GetTreeAsync();
+    }
+}

+ 115 - 0
RackPeek.Web/Components/Components/ServersListComponent.razor

@@ -0,0 +1,115 @@
+@using RackPeek.Domain.Resources.Hardware.Models
+@using RackPeek.Domain.Resources.Hardware.Servers
+@inject GetServersUseCase GetServers
+
+<PageTitle>CLI Monitor</PageTitle>
+
+<div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6">
+    <h1 class="text-lg text-zinc-100 mb-6">
+        CLI Hardware Monitor
+    </h1>
+
+    @if (_servers is null)
+    {
+        <div class="text-zinc-500">loading servers…</div>
+    }
+    else if (_servers.Count == 0)
+    {
+        <div class="text-zinc-500">no servers found</div>
+    }
+    else
+    {
+        <div class="space-y-4">
+            @foreach (var server in _servers)
+            {
+                <div class="border border-zinc-800 rounded p-4 bg-zinc-900">
+                    <div class="flex justify-between items-center mb-3">
+                        <div class="text-zinc-100">
+                            @server.Name
+                        </div>
+
+                        @if (server.Ipmi == true)
+                        {
+                            <span class="text-xs text-emerald-400">IPMI</span>
+                        }
+                    </div>
+
+                    <div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
+
+                        @if (server.Cpus?.Any() == true)
+                        {
+                            <div>
+                                <div class="text-zinc-400 mb-1">CPU</div>
+                                @foreach (var cpu in server.Cpus)
+                                {
+                                    <div class="text-zinc-300">
+                                        @cpu.Model — @cpu.Cores cores / @cpu.Threads threads
+                                    </div>
+                                }
+                            </div>
+                        }
+
+                        @if (server.Ram is not null)
+                        {
+                            <div>
+                                <div class="text-zinc-400 mb-1">RAM</div>
+                                <div class="text-zinc-300">
+                                    @server.Ram.Size GB @server.Ram.Mts MT/s
+                                </div>
+                            </div>
+                        }
+
+                        @if (server.Drives?.Any() == true)
+                        {
+                            <div>
+                                <div class="text-zinc-400 mb-1">Drives</div>
+                                @foreach (var drive in server.Drives)
+                                {
+                                    <div class="text-zinc-300">
+                                        @drive.Type — @drive.Size GB
+                                    </div>
+                                }
+                            </div>
+                        }
+
+                        @if (server.Nics?.Any() == true)
+                        {
+                            <div>
+                                <div class="text-zinc-400 mb-1">NICs</div>
+                                @foreach (var nic in server.Nics)
+                                {
+                                    <div class="text-zinc-300">
+                                        @nic.Type — @nic.Speed Gbps (@nic.Ports ports)
+                                    </div>
+                                }
+                            </div>
+                        }
+
+                        @if (server.Gpus?.Any() == true)
+                        {
+                            <div>
+                                <div class="text-zinc-400 mb-1">GPU</div>
+                                @foreach (var gpu in server.Gpus)
+                                {
+                                    <div class="text-zinc-300">
+                                        @gpu.Model — @gpu.Vram GB VRAM
+                                    </div>
+                                }
+                            </div>
+                        }
+
+                    </div>
+                </div>
+            }
+        </div>
+    }
+</div>
+
+@code {
+    private IReadOnlyList<Server>? _servers;
+
+    protected override async Task OnInitializedAsync()
+    {
+        _servers = await GetServers.ExecuteAsync();
+    }
+}

+ 3 - 0
RackPeek.Web/Components/Layout/MainLayout.razor

@@ -0,0 +1,3 @@
+@inherits LayoutComponentBase
+
+@Body

+ 20 - 0
RackPeek.Web/Components/Layout/MainLayout.razor.css

@@ -0,0 +1,20 @@
+#blazor-error-ui {
+    color-scheme: light only;
+    background: lightyellow;
+    bottom: 0;
+    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
+    box-sizing: border-box;
+    display: none;
+    left: 0;
+    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
+    position: fixed;
+    width: 100%;
+    z-index: 1000;
+}
+
+    #blazor-error-ui .dismiss {
+        cursor: pointer;
+        position: absolute;
+        right: 0.75rem;
+        top: 0.5rem;
+    }

+ 31 - 0
RackPeek.Web/Components/Layout/ReconnectModal.razor

@@ -0,0 +1,31 @@
+<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
+
+<dialog id="components-reconnect-modal" data-nosnippet>
+    <div class="components-reconnect-container">
+        <div class="components-rejoining-animation" aria-hidden="true">
+            <div></div>
+            <div></div>
+        </div>
+        <p class="components-reconnect-first-attempt-visible">
+            Rejoining the server...
+        </p>
+        <p class="components-reconnect-repeated-attempt-visible">
+            Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
+        </p>
+        <p class="components-reconnect-failed-visible">
+            Failed to rejoin.<br/>Please retry or reload the page.
+        </p>
+        <button id="components-reconnect-button" class="components-reconnect-failed-visible">
+            Retry
+        </button>
+        <p class="components-pause-visible">
+            The session has been paused by the server.
+        </p>
+        <button id="components-resume-button" class="components-pause-visible">
+            Resume
+        </button>
+        <p class="components-resume-failed-visible">
+            Failed to resume the session.<br/>Please reload the page.
+        </p>
+    </div>
+</dialog>

+ 157 - 0
RackPeek.Web/Components/Layout/ReconnectModal.razor.css

@@ -0,0 +1,157 @@
+.components-reconnect-first-attempt-visible,
+.components-reconnect-repeated-attempt-visible,
+.components-reconnect-failed-visible,
+.components-pause-visible,
+.components-resume-failed-visible,
+.components-rejoining-animation {
+    display: none;
+}
+
+#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
+#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
+#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
+#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
+#components-reconnect-modal.components-reconnect-retrying,
+#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
+#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
+#components-reconnect-modal.components-reconnect-failed,
+#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
+    display: block;
+}
+
+
+#components-reconnect-modal {
+    background-color: white;
+    width: 20rem;
+    margin: 20vh auto;
+    padding: 2rem;
+    border: 0;
+    border-radius: 0.5rem;
+    box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
+    opacity: 0;
+    transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
+    animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
+    &[open]
+
+{
+    animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
+    animation-fill-mode: both;
+}
+
+}
+
+#components-reconnect-modal::backdrop {
+    background-color: rgba(0, 0, 0, 0.4);
+    animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
+    opacity: 1;
+}
+
+@keyframes components-reconnect-modal-slideUp {
+    0% {
+        transform: translateY(30px) scale(0.95);
+    }
+
+    100% {
+        transform: translateY(0);
+    }
+}
+
+@keyframes components-reconnect-modal-fadeInOpacity {
+    0% {
+        opacity: 0;
+    }
+
+    100% {
+        opacity: 1;
+    }
+}
+
+@keyframes components-reconnect-modal-fadeOutOpacity {
+    0% {
+        opacity: 1;
+    }
+
+    100% {
+        opacity: 0;
+    }
+}
+
+.components-reconnect-container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 1rem;
+}
+
+#components-reconnect-modal p {
+    margin: 0;
+    text-align: center;
+}
+
+#components-reconnect-modal button {
+    border: 0;
+    background-color: #6b9ed2;
+    color: white;
+    padding: 4px 24px;
+    border-radius: 4px;
+}
+
+    #components-reconnect-modal button:hover {
+        background-color: #3b6ea2;
+    }
+
+    #components-reconnect-modal button:active {
+        background-color: #6b9ed2;
+    }
+
+.components-rejoining-animation {
+    position: relative;
+    width: 80px;
+    height: 80px;
+}
+
+    .components-rejoining-animation div {
+        position: absolute;
+        border: 3px solid #0087ff;
+        opacity: 1;
+        border-radius: 50%;
+        animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
+    }
+
+        .components-rejoining-animation div:nth-child(2) {
+            animation-delay: -0.5s;
+        }
+
+@keyframes components-rejoining-animation {
+    0% {
+        top: 40px;
+        left: 40px;
+        width: 0;
+        height: 0;
+        opacity: 0;
+    }
+
+    4.9% {
+        top: 40px;
+        left: 40px;
+        width: 0;
+        height: 0;
+        opacity: 0;
+    }
+
+    5% {
+        top: 40px;
+        left: 40px;
+        width: 0;
+        height: 0;
+        opacity: 1;
+    }
+
+    100% {
+        top: 0px;
+        left: 0px;
+        width: 80px;
+        height: 80px;
+        opacity: 0;
+    }
+}

+ 63 - 0
RackPeek.Web/Components/Layout/ReconnectModal.razor.js

@@ -0,0 +1,63 @@
+// Set up event handlers
+const reconnectModal = document.getElementById("components-reconnect-modal");
+reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
+
+const retryButton = document.getElementById("components-reconnect-button");
+retryButton.addEventListener("click", retry);
+
+const resumeButton = document.getElementById("components-resume-button");
+resumeButton.addEventListener("click", resume);
+
+function handleReconnectStateChanged(event) {
+    if (event.detail.state === "show") {
+        reconnectModal.showModal();
+    } else if (event.detail.state === "hide") {
+        reconnectModal.close();
+    } else if (event.detail.state === "failed") {
+        document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
+    } else if (event.detail.state === "rejected") {
+        location.reload();
+    }
+}
+
+async function retry() {
+    document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
+
+    try {
+        // Reconnect will asynchronously return:
+        // - true to mean success
+        // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
+        // - exception to mean we didn't reach the server (this can be sync or async)
+        const successful = await Blazor.reconnect();
+        if (!successful) {
+            // We have been able to reach the server, but the circuit is no longer available.
+            // We'll reload the page so the user can continue using the app as quickly as possible.
+            const resumeSuccessful = await Blazor.resumeCircuit();
+            if (!resumeSuccessful) {
+                location.reload();
+            } else {
+                reconnectModal.close();
+            }
+        }
+    } catch (err) {
+        // We got an exception, server is currently unavailable
+        document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
+    }
+}
+
+async function resume() {
+    try {
+        const successful = await Blazor.resumeCircuit();
+        if (!successful) {
+            location.reload();
+        }
+    } catch {
+        location.reload();
+    }
+}
+
+async function retryWhenDocumentBecomesVisible() {
+    if (document.visibilityState === "visible") {
+        await retry();
+    }
+}

+ 6 - 0
RackPeek.Web/Components/Pages/Error.razor

@@ -0,0 +1,6 @@
+@page "/Error"
+
+<PageTitle>Error</PageTitle>
+
+<h1 class="text-danger">Error.</h1>
+<h2 class="text-danger">An error occurred while processing your request.</h2>

+ 6 - 0
RackPeek.Web/Components/Pages/Home.razor

@@ -0,0 +1,6 @@
+@page "/"
+@using RackPeek.Web.Components.Components
+
+<PageTitle>CLI Monitor</PageTitle>
+
+<ServersListComponent/>

+ 5 - 0
RackPeek.Web/Components/Pages/NotFound.razor

@@ -0,0 +1,5 @@
+@page "/not-found"
+@layout MainLayout
+
+<h3>Not Found</h3>
+<p>Sorry, the content you are looking for does not exist.</p>

+ 6 - 0
RackPeek.Web/Components/Routes.razor

@@ -0,0 +1,6 @@
+<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
+    <Found Context="routeData">
+        <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/>
+        <FocusOnNavigate RouteData="routeData" Selector="h1"/>
+    </Found>
+</Router>

+ 11 - 0
RackPeek.Web/Components/_Imports.razor

@@ -0,0 +1,11 @@
+@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.JSInterop
+@using RackPeek.Web
+@using RackPeek.Web.Components
+@using RackPeek.Web.Components.Layout

+ 78 - 0
RackPeek.Web/Program.cs

@@ -0,0 +1,78 @@
+using Microsoft.AspNetCore.Hosting.StaticWebAssets;
+using RackPeek.Domain;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Services;
+using RackPeek.Domain.Resources.SystemResources;
+using RackPeek.Web.Components;
+using RackPeek.Yaml;
+
+namespace RackPeek.Web;
+
+public class Program
+{
+    public static void Main(string[] args)
+    {
+        var builder = WebApplication.CreateBuilder(args);
+
+        StaticWebAssetsLoader.UseStaticWebAssets(
+            builder.Environment,
+            builder.Configuration
+        );
+
+        var yamlDir = "./config";
+        
+        var collection = new YamlResourceCollection();
+        var basePath = Directory.GetCurrentDirectory();
+
+        // Resolve yamlDir as relative to basePath
+        var yamlPath = Path.IsPathRooted(yamlDir)
+            ? yamlDir
+            : Path.Combine(basePath, yamlDir);
+
+        if (!Directory.Exists(yamlPath))
+            throw new DirectoryNotFoundException(
+                $"YAML directory not found: {yamlPath}"
+            );
+
+        // Load all .yml and .yaml files
+        var yamlFiles = Directory.EnumerateFiles(yamlPath, "*.yml")
+            .Concat(Directory.EnumerateFiles(yamlPath, "*.yaml"))
+            .ToArray();
+        
+        collection.LoadFiles(yamlFiles.Select(f => Path.Combine(basePath, f)));
+
+        // Infrastructure
+        builder.Services.AddScoped<IHardwareRepository>(_ => new YamlHardwareRepository(collection));
+        builder.Services.AddScoped<ISystemRepository>(_ => new YamlSystemRepository(collection));
+        builder.Services.AddScoped<IServiceRepository>(_ => new YamlServiceRepository(collection));
+
+        
+        builder.Services.AddUseCases();
+
+        // Add services to the container.
+        builder.Services.AddRazorComponents()
+            .AddInteractiveServerComponents();
+        
+        var app = builder.Build();
+
+        // Configure the HTTP request pipeline.
+        if (!app.Environment.IsDevelopment())
+        {
+            app.UseExceptionHandler("/Error");
+            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
+            app.UseHsts();
+        }
+
+        app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
+        app.UseHttpsRedirection();
+        app.UseStaticFiles();
+
+        app.UseAntiforgery();
+
+        app.MapStaticAssets();
+        app.MapRazorComponents<App>()
+            .AddInteractiveServerRenderMode();
+
+        app.Run();
+    }
+}

+ 23 - 0
RackPeek.Web/Properties/launchSettings.json

@@ -0,0 +1,23 @@
+{
+  "$schema": "https://json.schemastore.org/launchsettings.json",
+    "profiles": {
+      "http": {
+        "commandName": "Project",
+        "dotnetRunMessages": true,
+        "launchBrowser": true,
+        "applicationUrl": "http://localhost:5287",
+        "environmentVariables": {
+          "ASPNETCORE_ENVIRONMENT": "Development"
+        }
+      },
+      "https": {
+        "commandName": "Project",
+        "dotnetRunMessages": true,
+        "launchBrowser": true,
+        "applicationUrl": "https://localhost:7083;http://localhost:5287",
+        "environmentVariables": {
+          "ASPNETCORE_ENVIRONMENT": "Development"
+        }
+      }
+    }
+  }

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

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+    <PropertyGroup>
+        <TargetFramework>net10.0</TargetFramework>
+        <Nullable>enable</Nullable>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\RackPeek\RackPeek.csproj" />
+    </ItemGroup>
+
+</Project>

+ 8 - 0
RackPeek.Web/appsettings.Development.json

@@ -0,0 +1,8 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft.AspNetCore": "Warning"
+    }
+  }
+}

+ 9 - 0
RackPeek.Web/appsettings.json

@@ -0,0 +1,9 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft.AspNetCore": "Warning"
+    }
+  },
+  "AllowedHosts": "*"
+}

+ 585 - 0
RackPeek.Web/config/Services.yaml

@@ -0,0 +1,585 @@
+resources:
+  - kind: Service
+    name: immich
+    network:
+      ip: 192.168.0.4
+      port: 8080
+      protocol: TCP
+      url: http://immich.lan:8080
+    runsOn: proxmox-host
+
+  - kind: Service
+    name: jellyfin
+    network:
+      ip: 192.168.0.10
+      port: 8096
+      protocol: TCP
+      url: http://jellyfin.lan:8096
+    runsOn: docker-host
+
+  - kind: Service
+    name: plex
+    network:
+      ip: 192.168.0.11
+      port: 32400
+      protocol: TCP
+      url: http://plex.lan:32400
+    runsOn: proxmox-host
+
+  - kind: Service
+    name: home-assistant
+    network:
+      ip: 192.168.1.20
+      port: 8123
+      protocol: TCP
+      url: http://ha.lan:8123
+    runsOn: k8s-node-1
+
+  - kind: Service
+    name: pihole
+    network:
+      ip: 192.168.1.2
+      port: 53
+      protocol: UDP
+      url: http://pihole.lan/admin
+    runsOn: baremetal-rpi4
+
+  - kind: Service
+    name: unifi-controller
+    network:
+      ip: 192.168.1.5
+      port: 8443
+      protocol: TCP
+      url: https://unifi.lan:8443
+    runsOn: vm-cluster-1
+
+  - kind: Service
+    name: syncthing
+    network:
+      ip: 10.0.0.15
+      port: 8384
+      protocol: TCP
+      url: http://sync.internal:8384
+    runsOn: docker-host
+
+  - kind: Service
+    name: grafana
+    network:
+      ip: 10.0.0.20
+      port: 3000
+      protocol: TCP
+      url: http://grafana.internal:3000
+    runsOn: monitoring-node
+
+  - kind: Service
+    name: prometheus
+    network:
+      ip: 10.0.0.21
+      port: 9090
+      protocol: TCP
+      url: http://prometheus.internal:9090
+    runsOn: monitoring-node
+
+  - kind: Service
+    name: loki
+    network:
+      ip: 10.0.0.22
+      port: 3100
+      protocol: TCP
+      url: http://loki.internal:3100
+    runsOn: monitoring-node
+
+  - kind: Service
+    name: minio
+    network:
+      ip: 172.16.0.10
+      port: 9000
+      protocol: TCP
+      url: http://minio.storage:9000
+    runsOn: storage-node-1
+
+  - kind: Service
+    name: nextcloud
+    network:
+      ip: 172.16.0.11
+      port: 443
+      protocol: TCP
+      url: https://nextcloud.storage
+    runsOn: storage-node-2
+
+  - kind: Service
+    name: vaultwarden
+    network:
+      ip: 192.168.0.30
+      port: 8081
+      protocol: TCP
+      url: http://vault.lan:8081
+    runsOn: docker-host
+
+  - kind: Service
+    name: traefik
+    network:
+      ip: 192.168.0.2
+      port: 80
+      protocol: TCP
+      url: http://traefik.lan
+    runsOn: k8s-node-1
+
+  - kind: Service
+    name: nginx-reverse-proxy
+    network:
+      ip: 192.168.0.3
+      port: 443
+      protocol: TCP
+      url: https://proxy.lan
+    runsOn: docker-host
+
+  - kind: Service
+    name: qbittorrent
+    network:
+      ip: 192.168.0.40
+      port: 8080
+      protocol: TCP
+      url: http://torrent.lan:8080
+    runsOn: proxmox-host
+
+  - kind: Service
+    name: radarr
+    network:
+      ip: 192.168.0.41
+      port: 7878
+      protocol: TCP
+      url: http://radarr.lan:7878
+    runsOn: docker-host
+
+  - kind: Service
+    name: sonarr
+    network:
+      ip: 192.168.0.42
+      port: 8989
+      protocol: TCP
+      url: http://sonarr.lan:8989
+    runsOn: docker-host
+
+  - kind: Service
+    name: prowlarr
+    network:
+      ip: 192.168.0.43
+      port: 9696
+      protocol: TCP
+      url: http://prowlarr.lan:9696
+    runsOn: docker-host
+
+  - kind: Service
+    name: sabnzbd
+    network:
+      ip: 192.168.0.44
+      port: 8085
+      protocol: TCP
+      url: http://sabnzbd.lan:8085
+    runsOn: docker-host
+
+  - kind: Service
+    name: frigate
+    network:
+      ip: 192.168.1.30
+      port: 5000
+      protocol: TCP
+      url: http://frigate.lan:5000
+    runsOn: k8s-node-2
+
+  - kind: Service
+    name: mosquitto-mqtt
+    network:
+      ip: 192.168.1.31
+      port: 1883
+      protocol: TCP
+      url: mqtt://mqtt.lan:1883
+    runsOn: docker-host
+
+  - kind: Service
+    name: zigbee2mqtt
+    network:
+      ip: 192.168.1.32
+      port: 8080
+      protocol: TCP
+      url: http://z2m.lan:8080
+    runsOn: docker-host
+
+  - kind: Service
+    name: postgres-main
+    network:
+      ip: 10.0.1.10
+      port: 5432
+      protocol: TCP
+      url: postgres://db.internal:5432
+    runsOn: db-node-1
+
+  - kind: Service
+    name: mariadb
+    network:
+      ip: 10.0.1.11
+      port: 3306
+      protocol: TCP
+      url: mysql://mariadb.internal:3306
+    runsOn: db-node-2
+
+  - kind: Service
+    name: redis-cache
+    network:
+      ip: 10.0.1.12
+      port: 6379
+      protocol: TCP
+      url: redis://redis.internal:6379
+    runsOn: cache-node
+
+  - kind: Service
+    name: elasticsearch
+    network:
+      ip: 10.0.2.10
+      port: 9200
+      protocol: TCP
+      url: http://es.internal:9200
+    runsOn: search-node
+
+  - kind: Service
+    name: kibana
+    network:
+      ip: 10.0.2.11
+      port: 5601
+      protocol: TCP
+      url: http://kibana.internal:5601
+    runsOn: search-node
+
+  - kind: Service
+    name: uptime-kuma
+    network:
+      ip: 192.168.0.50
+      port: 3001
+      protocol: TCP
+      url: http://uptime.lan:3001
+    runsOn: docker-host
+
+  - kind: Service
+    name: wireguard-vpn
+    network:
+      ip: 192.168.1.100
+      port: 51820
+      protocol: UDP
+      url: wg://vpn.lan
+    runsOn: baremetal-rpi4
+
+  - kind: Service
+    name: openvpn
+    network:
+      ip: 192.168.1.101
+      port: 1194
+      protocol: UDP
+      url: ovpn://openvpn.lan
+    runsOn: vm-cluster-2
+
+  - kind: Service
+    name: adguard-home
+    network:
+      ip: 192.168.1.3
+      port: 3000
+      protocol: TCP
+      url: http://adguard.lan:3000
+    runsOn: docker-host
+
+  - kind: Service
+    name: gitlab
+    network:
+      ip: 10.0.3.10
+      port: 443
+      protocol: TCP
+      url: https://gitlab.internal
+    runsOn: dev-node-1
+
+  - kind: Service
+    name: gitea
+    network:
+      ip: 10.0.3.11
+      port: 3000
+      protocol: TCP
+      url: http://gitea.internal:3000
+    runsOn: dev-node-2
+
+  - kind: Service
+    name: drone-ci
+    network:
+      ip: 10.0.3.12
+      port: 8080
+      protocol: TCP
+      url: http://drone.internal:8080
+    runsOn: dev-node-2
+
+  - kind: Service
+    name: harbor-registry
+    network:
+      ip: 10.0.3.13
+      port: 5000
+      protocol: TCP
+      url: http://harbor.internal:5000
+    runsOn: dev-node-3
+
+  - kind: Service
+    name: kubernetes-api
+    network:
+      ip: 10.0.4.1
+      port: 6443
+      protocol: TCP
+      url: https://k8s-api.internal:6443
+    runsOn: k8s-control-plane
+
+  - kind: Service
+    name: longhorn-ui
+    network:
+      ip: 10.0.4.20
+      port: 9500
+      protocol: TCP
+      url: http://longhorn.internal:9500
+    runsOn: k8s-node-3
+
+  - kind: Service
+    name: rook-ceph-dashboard
+    network:
+      ip: 10.0.4.21
+      port: 8443
+      protocol: TCP
+      url: https://ceph.internal:8443
+    runsOn: k8s-node-3
+
+  - kind: Service
+    name: samba-fileserver
+    network:
+      ip: 192.168.0.60
+      port: 445
+      protocol: TCP
+      url: smb://fileserver.lan
+    runsOn: storage-node-1
+
+  - kind: Service
+    name: nfs-server
+    network:
+      ip: 192.168.0.61
+      port: 2049
+      protocol: TCP
+      url: nfs://nfs.lan
+    runsOn: dell-c6400-node01
+
+  - kind: Service
+    name: iscsi-target
+    network:
+      ip: 172.16.1.10
+      port: 3260
+      protocol: TCP
+      url: iscsi://iscsi.storage
+    runsOn: storage-node-3
+
+  - kind: Service
+    name: calibre-web
+    network:
+      ip: 192.168.0.70
+      port: 8083
+      protocol: TCP
+      url: http://books.lan:8083
+    runsOn: docker-host
+
+  - kind: Service
+    name: paperless-ngx
+    network:
+      ip: 192.168.0.71
+      port: 8000
+      protocol: TCP
+      url: http://docs.lan:8000
+    runsOn: dell-c6400-node01
+
+  - kind: Service
+    name: openldap
+    network:
+      ip: 10.0.5.10
+      port: 389
+      protocol: TCP
+      url: ldap://ldap.internal:389
+    runsOn: dell-c6400-node01
+
+  - kind: Service
+    name: keycloak
+    network:
+      ip: 10.0.5.11
+      port: 8080
+      protocol: TCP
+      url: http://keycloak.internal:8080
+    runsOn: dell-c6400-node01
+
+  - kind: Service
+    name: ntp-server
+    network:
+      ip: 192.168.1.50
+      port: 123
+      protocol: UDP
+      url: ntp://ntp.lan
+    runsOn: baremetal-rpi3
+
+  - kind: Service
+    name: syslog-server
+    network:
+      ip: 10.0.6.10
+      port: 514
+      protocol: UDP
+      url: syslog://syslog.internal
+    runsOn: monitoring-node
+
+  - kind: Service
+    name: dhcp-server
+    network:
+      ip: 192.168.1.1
+      port: 67
+      protocol: UDP
+      url: dhcp://dhcp.lan
+    runsOn: router-appliance
+
+  - kind: Service
+    name: bind-dns
+    network:
+      ip: 10.0.7.10
+      port: 53
+      protocol: UDP
+      url: dns://dns.internal
+    runsOn: infra-node
+
+  - kind: Service
+    name: vault
+    network:
+      ip: 10.0.7.11
+      port: 8200
+      protocol: TCP
+      url: http://vault.internal:8200
+    runsOn: infra-node
+
+  - kind: Service
+    name: consul
+    network:
+      ip: 10.0.7.12
+      port: 8500
+      protocol: TCP
+      url: http://consul.internal:8500
+    runsOn: infra-node
+
+  - kind: Service
+    name: nomad
+    network:
+      ip: 10.0.7.13
+      port: 4646
+      protocol: TCP
+      url: http://nomad.internal:4646
+    runsOn: infra-node
+
+  - kind: Service
+    name: openhab
+    network:
+      ip: 192.168.1.40
+      port: 8080
+      protocol: TCP
+      url: http://openhab.lan:8080
+    runsOn: k8s-node-2
+
+  - kind: Service
+    name: mqtt-explorer
+    network:
+      ip: 192.168.1.41
+      port: 4000
+      protocol: TCP
+      url: http://mqtt-explorer.lan:4000
+    runsOn: docker-host
+
+  - kind: Service
+    name: influxdb
+    network:
+      ip: 10.0.8.10
+      port: 8086
+      protocol: TCP
+      url: http://influx.internal:8086
+    runsOn: monitoring-node
+
+  - kind: Service
+    name: telegraf
+    network:
+      ip: 10.0.8.11
+      port: 8125
+      protocol: UDP
+      url: statsd://telegraf.internal
+    runsOn: monitoring-node
+
+  - kind: Service
+    name: speedtest-tracker
+    network:
+      ip: 192.168.0.80
+      port: 8080
+      protocol: TCP
+      url: http://speedtest.lan:8080
+    runsOn: docker-host
+
+  - kind: Service
+    name: navidrome
+    network:
+      ip: 192.168.0.81
+      port: 4533
+      protocol: TCP
+      url: http://music.lan:4533
+    runsOn: docker-host
+
+  - kind: Service
+    name: photoprism
+    network:
+      ip: 192.168.0.82
+      port: 2342
+      protocol: TCP
+      url: http://photos.lan:2342
+    runsOn: docker-host
+
+  - kind: Service
+    name: dnsdist
+    network:
+      ip: 10.0.9.10
+      port: 53
+      protocol: UDP
+      url: dns://dnsdist.internal
+    runsOn: infra-node
+
+  - kind: Service
+    name: powerdns
+    network:
+      ip: 10.0.9.11
+      port: 8081
+      protocol: TCP
+      url: http://pdns.internal:8081
+    runsOn: infra-node
+
+  - kind: Service
+    name: openproject
+    network:
+      ip: 10.0.10.10
+      port: 8080
+      protocol: TCP
+      url: http://openproject.internal:8080
+    runsOn: dev-node-3
+
+  - kind: Service
+    name: mattermost
+    network:
+      ip: 10.0.10.11
+      port: 8065
+      protocol: TCP
+      url: http://chat.internal:8065
+    runsOn: dev-node-3
+
+  - kind: Service
+    name: rocket-chat
+    network:
+      ip: 10.0.10.12
+      port: 3000
+      protocol: TCP
+      url: http://rocket.internal:3000
+    runsOn: dev-node-3

+ 184 - 0
RackPeek.Web/config/Systems.yaml

@@ -0,0 +1,184 @@
+resources:
+  - kind: System
+    type: Hypervisor
+    name: proxmox-host
+    os: proxmox
+    cores: 16
+    ram: 64gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: ContainerHost
+    name: docker-host
+    os: ubuntu
+    cores: 12
+    ram: 32gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: KubernetesNode
+    name: k8s-node-1
+    os: ubuntu
+    cores: 8
+    ram: 32gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: KubernetesNode
+    name: k8s-node-2
+    os: ubuntu
+    cores: 8
+    ram: 32gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: KubernetesNode
+    name: k8s-node-3
+    os: ubuntu
+    cores: 8
+    ram: 32gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: KubernetesControlPlane
+    name: k8s-control-plane
+    os: ubuntu
+    cores: 4
+    ram: 16gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: Monitoring
+    name: monitoring-node
+    os: ubuntu
+    cores: 8
+    ram: 32gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: Storage
+    name: storage-node-1
+    os: truenas
+    cores: 8
+    ram: 64gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: Storage
+    name: storage-node-2
+    os: truenas
+    cores: 8
+    ram: 64gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: Storage
+    name: storage-node-3
+    os: truenas
+    cores: 8
+    ram: 64gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: Database
+    name: db-node-1
+    os: ubuntu
+    cores: 8
+    ram: 32gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: Database
+    name: db-node-2
+    os: ubuntu
+    cores: 8
+    ram: 32gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: Cache
+    name: cache-node
+    os: ubuntu
+    cores: 4
+    ram: 16gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: Search
+    name: search-node
+    os: ubuntu
+    cores: 8
+    ram: 32gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: Development
+    name: dev-node-1
+    os: ubuntu
+    cores: 4
+    ram: 16gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: Development
+    name: dev-node-2
+    os: ubuntu
+    cores: 4
+    ram: 16gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: Development
+    name: dev-node-3
+    os: ubuntu
+    cores: 6
+    ram: 24gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: VirtualMachineCluster
+    name: vm-cluster-1
+    os: proxmox
+    cores: 12
+    ram: 48gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: VirtualMachineCluster
+    name: vm-cluster-2
+    os: proxmox
+    cores: 12
+    ram: 48gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: BareMetal
+    name: baremetal-rpi4
+    os: raspbian
+    cores: 4
+    ram: 8gb
+    runsOn: rack-edge
+
+  - kind: System
+    type: BareMetal
+    name: baremetal-rpi3
+    os: raspbian
+    cores: 4
+    ram: 4gb
+    runsOn: rack-edge
+
+  - kind: System
+    type: Infrastructure
+    name: infra-node
+    os: ubuntu
+    cores: 4
+    ram: 16gb
+    runsOn: dell-c6400-node01
+
+  - kind: System
+    type: NetworkAppliance
+    name: router-appliance
+    os: openwrt
+    cores: 2
+    ram: 2gb
+    runsOn: network-rack

+ 31 - 0
RackPeek.Web/config/accesspoints.yaml

@@ -0,0 +1,31 @@
+resources:
+  - kind: AccessPoint
+    model: Unifi-Ap-Pro
+    speed: 1
+    name: lounge-ap
+    tags:
+  - kind: AccessPoint
+    model: Unifi-U6-Lite
+    speed: 1
+    name: office-ap
+    tags:
+  - kind: AccessPoint
+    model: TP-Link-EAP245
+    speed: 1
+    name: garage-ap
+    tags:
+  - kind: AccessPoint
+    model: Aruba-AP-515
+    speed: 2.5
+    name: upstairs-ap
+    tags:
+  - kind: AccessPoint
+    model: Unifi-U6-Mesh
+    speed: 1
+    name: guest-ap
+    tags:
+  - kind: AccessPoint
+    model: Cisco-Aironet-1832i
+    speed: 1
+    name: warehouse-ap
+    tags: 

+ 21 - 0
RackPeek.Web/config/desktops.yaml

@@ -0,0 +1,21 @@
+resources:
+  - kind: Desktop
+    cpus:
+      - model: Intel(R) Core(TM) i5-9500
+        cores: 6
+        threads: 6
+    ram:
+      size: 16
+      mts: 2666
+    drives:
+      - type: ssd
+        size: 512
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 1
+    gpus:
+      - model: RTX 3080
+        vram: 12
+    name: dell-optiplex
+    tags: 

+ 14 - 0
RackPeek.Web/config/firewalls.yaml

@@ -0,0 +1,14 @@
+resources:
+  - kind: Firewall
+    model: pfSense-1100
+    managed: true
+    poe: true
+    ports:
+      - type: rj45
+        speed: 1
+        count: 8
+      - type: sfp
+        speed: 10
+        count: 2
+    name: pfsense
+    tags: 

+ 17 - 0
RackPeek.Web/config/laptops.yaml

@@ -0,0 +1,17 @@
+resources:
+  - kind: Laptop
+    cpus:
+      - model: Intel(R) Core(TM) i7-10510U
+        cores: 4
+        threads: 8
+    ram:
+      size: 16
+      mts: 2666
+    drives:
+      - type: ssd
+        size: 1024
+    gpus:
+      - model: RTX 3080
+        vram: 12
+    name: thinkpad-x1
+    tags: 

+ 14 - 0
RackPeek.Web/config/routers.yaml

@@ -0,0 +1,14 @@
+resources:
+  - kind: Router
+    model: ER-4
+    managed: true
+    poe: true
+    ports:
+      - type: rj45
+        speed: 1
+        count: 8
+      - type: sfp
+        speed: 10
+        count: 2
+    name: ubiquiti-edge-router
+    tags: 

+ 397 - 0
RackPeek.Web/config/servers.yaml

@@ -0,0 +1,397 @@
+resources:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Xeon(R) Silver 4110
+        cores: 8
+        threads: 16
+    ram:
+      size: 64
+      mts: 2400
+    drives:
+      - type: ssd
+        size: 480
+      - type: ssd
+        size: 480
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 2
+      - type: sfp+
+        speed: 10
+        ports: 2
+    gpus:
+    ipmi: true
+    name: dell-c6400-node01
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Xeon(R) Silver 4110
+        cores: 8
+        threads: 16
+    ram:
+      size: 128
+      mts: 2400
+    drives:
+      - type: ssd
+        size: 960
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 2
+      - type: sfp+
+        speed: 10
+        ports: 2
+    gpus:
+    ipmi: true
+    name: dell-c6400-node02
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Xeon(R) Silver 4110
+        cores: 8
+        threads: 16
+    ram:
+      size: 64
+      mts: 2400
+    drives:
+      - type: ssd
+        size: 480
+      - type: ssd
+        size: 480
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 2
+      - type: sfp+
+        speed: 10
+        ports: 2
+    gpus:
+    ipmi: true
+    name: dell-c6400-node03
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Xeon(R) Silver 4110
+        cores: 8
+        threads: 16
+    ram:
+      size: 128
+      mts: 2400
+    drives:
+      - type: ssd
+        size: 960
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 2
+      - type: sfp+
+        speed: 10
+        ports: 2
+    gpus:
+    ipmi: true
+    name: dell-c6400-node04
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Xeon(R) E5-2620 v4
+        cores: 8
+        threads: 16
+    ram:
+      size: 64
+      mts: 2133
+    drives:
+      - type: hdd
+        size: 8192
+      - type: hdd
+        size: 8192
+      - type: hdd
+        size: 8192
+      - type: hdd
+        size: 8192
+      - type: ssd
+        size: 120
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 1
+      - type: sfp+
+        speed: 10
+        ports: 1
+    gpus:
+    ipmi: true
+    name: truenas-storage01
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Core(TM) i5-8500
+        cores: 6
+        threads: 6
+    ram:
+      size: 32
+      mts: 2666
+    drives:
+      - type: ssd
+        size: 512
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 4
+    gpus:
+    ipmi: false
+    name: proxmox-edge01
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Celeron(R) J4125
+        cores: 4
+        threads: 4
+    ram:
+      size: 8
+      mts: 2400
+    drives:
+      - type: ssd
+        size: 64
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 4
+    gpus:
+    ipmi: false
+    name: opnsense-fw01
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Xeon(R) E3-1270 v6
+        cores: 4
+        threads: 8
+    ram:
+      size: 16
+      mts: 2400
+    drives:
+      - type: ssd
+        size: 256
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 1
+    gpus:
+    ipmi: true
+    name: mgmt-bastion01
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Xeon(R) E5-2630 v4
+        cores: 10
+        threads: 20
+    ram:
+      size: 64
+      mts: 2133
+    drives:
+      - type: hdd
+        size: 6144
+      - type: hdd
+        size: 6144
+      - type: hdd
+        size: 6144
+      - type: hdd
+        size: 6144
+      - type: ssd
+        size: 240
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 2
+      - type: sfp+
+        speed: 10
+        ports: 1
+    gpus:
+    ipmi: true
+    name: truenas-backup01
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Xeon(R) Silver 4214
+        cores: 12
+        threads: 24
+    ram:
+      size: 128
+      mts: 2666
+    drives:
+      - type: ssd
+        size: 1024
+    nics:
+      - type: sfp+
+        speed: 10
+        ports: 2
+    gpus:
+      - model: NVIDIA Tesla P40
+        vram: 24
+      - model: NVIDIA Tesla P40
+        vram: 24
+      - model: NVIDIA Tesla P4
+        vram: 8
+    ipmi: true
+    name: compute-gpu01
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Xeon(R) E3-1240 v5
+        cores: 4
+        threads: 8
+    ram:
+      size: 32
+      mts: 2133
+    drives:
+      - type: ssd
+        size: 512
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 2
+    gpus:
+    ipmi: true
+    name: proxmox-lab01
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Xeon(R) E-2224
+        cores: 4
+        threads: 4
+    ram:
+      size: 16
+      mts: 2666
+    drives:
+      - type: ssd
+        size: 256
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 2
+    gpus:
+    ipmi: true
+    name: k8s-control01
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Xeon(R) E-2224
+        cores: 4
+        threads: 4
+    ram:
+      size: 16
+      mts: 2666
+    drives:
+      - type: ssd
+        size: 256
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 2
+    gpus:
+    ipmi: true
+    name: k8s-control02
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Xeon(R) Silver 4108
+        cores: 8
+        threads: 16
+    ram:
+      size: 64
+      mts: 2400
+    drives:
+      - type: ssd
+        size: 1024
+      - type: ssd
+        size: 1024
+    nics:
+      - type: sfp+
+        speed: 10
+        ports: 1
+    gpus:
+    ipmi: true
+    name: elk-logging01
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Core(TM) i3-8100
+        cores: 4
+        threads: 4
+    ram:
+      size: 16
+      mts: 2400
+    drives:
+      - type: ssd
+        size: 256
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 2
+    gpus:
+    ipmi: false
+    name: edge-node01
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Xeon(R) E5-1650 v3
+        cores: 6
+        threads: 12
+    ram:
+      size: 64
+      mts: 2133
+    drives:
+      - type: ssd
+        size: 480
+      - type: hdd
+        size: 4096
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 4
+    gpus:
+    ipmi: true
+    name: backup-proxmox01
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Core(TM) i7-8700
+        cores: 6
+        threads: 12
+    ram:
+      size: 32
+      mts: 2666
+    drives:
+      - type: ssd
+        size: 512
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 1
+    gpus:
+    ipmi: false
+    name: lab-general01
+    tags:
+  - kind: Server
+    cpus:
+      - model: Intel(R) Xeon(R) E5-2650 v3
+        cores: 10
+        threads: 20
+    ram:
+      size: 128
+      mts: 2133
+    drives:
+      - type: hdd
+        size: 4096
+      - type: hdd
+        size: 4096
+      - type: hdd
+        size: 4096
+      - type: hdd
+        size: 4096
+    nics:
+      - type: sfp+
+        speed: 10
+        ports: 2
+    gpus:
+    ipmi: true
+    name: dell-r730-archive01
+    tags: 

+ 14 - 0
RackPeek.Web/config/switches.yaml

@@ -0,0 +1,14 @@
+resources:
+  - kind: Switch
+    model: GS324
+    managed: true
+    poe: true
+    ports:
+      - type: rj45
+        speed: 1
+        count: 8
+      - type: sfp
+        speed: 10
+        count: 2
+    name: netgear-s24
+    tags: 

+ 6 - 0
RackPeek.Web/config/ups.yaml

@@ -0,0 +1,6 @@
+resources:
+  - kind: Ups
+    model: Volta
+    va: 2200
+    name: rack-ups
+    tags: 

+ 38 - 0
RackPeek.Web/wwwroot/app.css

@@ -0,0 +1,38 @@
+h1:focus {
+    outline: none;
+}
+
+.valid.modified:not([type=checkbox]) {
+    outline: 1px solid #26b050;
+}
+
+.invalid {
+    outline: 1px solid #e50000;
+}
+
+.validation-message {
+    color: #e50000;
+}
+
+.blazor-error-boundary {
+    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
+    padding: 1rem 1rem 1rem 3.7rem;
+    color: white;
+}
+
+    .blazor-error-boundary::after {
+        content: "An error has occurred."
+    }
+
+.darker-border-checkbox.form-check-input {
+    border-color: #929292;
+}
+
+.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
+    color: var(--bs-secondary-color);
+    text-align: end;
+}
+
+.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
+    text-align: start;
+}

Разлика између датотеке није приказан због своје велике величине
+ 6 - 0
RackPeek.Web/wwwroot/tailwind.js


+ 6 - 0
RackPeek.sln

@@ -6,6 +6,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RackPeek.Domain", "RackPeek.Domain\RackPeek.Domain.csproj", "{760E6165-A7E0-4144-B289-1BAB6483FD29}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RackPeek.Web", "RackPeek.Web\RackPeek.Web.csproj", "{7E5A9ABE-B350-4D1B-86E5-03D9CDF44F84}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -24,5 +26,9 @@ Global
 		{760E6165-A7E0-4144-B289-1BAB6483FD29}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{760E6165-A7E0-4144-B289-1BAB6483FD29}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{760E6165-A7E0-4144-B289-1BAB6483FD29}.Release|Any CPU.Build.0 = Release|Any CPU
+		{7E5A9ABE-B350-4D1B-86E5-03D9CDF44F84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7E5A9ABE-B350-4D1B-86E5-03D9CDF44F84}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7E5A9ABE-B350-4D1B-86E5-03D9CDF44F84}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7E5A9ABE-B350-4D1B-86E5-03D9CDF44F84}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 EndGlobal

BIN
RackPeek/.DS_Store


+ 52 - 0
RackPeek/Yaml/YamlHardwareRepository.cs

@@ -3,6 +3,7 @@ using RackPeek.Domain.Resources.Hardware.Models;
 
 namespace RackPeek.Yaml;
 
+
 public class YamlHardwareRepository(YamlResourceCollection resources) : IHardwareRepository
 {
     public Task<IReadOnlyList<Hardware>> GetAllAsync()
@@ -14,6 +15,57 @@ public class YamlHardwareRepository(YamlResourceCollection resources) : IHardwar
     {
         return Task.FromResult(resources.GetByName(name) as Hardware);
     }
+    
+    public Task<List<HardwareTree>> GetTreeAsync()
+    {
+        var hardwareTree = new List<HardwareTree>();
+
+        var systemGroups = resources.SystemResources
+            .Where(s => !string.IsNullOrWhiteSpace(s.RunsOn))
+            .GroupBy(s => s.RunsOn!.Trim(), StringComparer.OrdinalIgnoreCase)
+            .ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
+
+        var serviceGroups = resources.ServiceResources
+            .Where(s => !string.IsNullOrWhiteSpace(s.RunsOn))
+            .GroupBy(s => s.RunsOn!.Trim(), StringComparer.OrdinalIgnoreCase)
+            .ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
+
+        foreach (var 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)
+                {
+                    var services = new List<string>();
+                    var systemKey = system.Name.Trim();
+
+                    if (serviceGroups.TryGetValue(systemKey, out var serviceResources))
+                    {
+                        services.AddRange(serviceResources.Select(s => s.Name));
+                    }
+
+                    systems.Add(new SystemTree
+                    {
+                        SystemName = system.Name,
+                        Services = services
+                    });
+                }
+            }
+
+            hardwareTree.Add(new HardwareTree
+            {
+                Kind = hardware.Kind,
+                HardwareName = hardware.Name,
+                Systems = systems
+            });
+        }
+
+        return Task.FromResult(hardwareTree);
+    }
+
 
     public Task AddAsync(Hardware hardware)
     {

Неке датотеке нису приказане због велике количине промена