@page "/subnets" @using RackPeek.Domain.Persistence @using RackPeek.Domain.Resources @using RackPeek.Domain.Resources.SystemResources @inject IResourceCollection ResourceCollection

Subnet Browser

@if (_grouped is not null && _grouped.Any()) { var nextFreeIp = GetNextFreeIp(); var nextFreePort = GetNextFreePort();
next free ip: @nextFreeIp
next free port: @nextFreePort
} @if (_grouped is null) {
loading…
} else if (!_grouped.Any()) {
no matching IPs found
} else {
@foreach (var subnetGroup in _grouped.OrderBy(x => IpToSortable(x.Key.Replace(".x", ".0")))) {
@{ var addresses = subnetGroup.Value.Count; var services = subnetGroup.Value .SelectMany(x => x.Value) .Count(x => x.Item1 is Service); // /24 assumption (x subnet) const int subnetCapacity = 256; var percentFull = (int)Math.Round((double)addresses / subnetCapacity * 100); }
@subnetGroup.Key (@addresses addresses, @services services, @percentFull% full)
    @foreach (var ipGroup in subnetGroup.Value.OrderBy(x => IpToSortable(x.Key))) {
  • ├─ @ipGroup.Key
      @foreach (var (resource, _) in ipGroup.Value) { var url = GetResourceUrl(resource); var port = resource is Service { Network.Port: not null } service ? service.Network!.Port : null; var typeName = resource.GetType().Name.Replace("Resource", "");
    • @resource.Name @if (port.HasValue) { :@port } (@typeName)
    • }
  • }
}
}
@code { private string _filter = string.Empty; private IReadOnlyList<(Resource resource, string ip)> _all = []; private Dictionary>>? _grouped; protected override async Task OnInitializedAsync() { _all = await ResourceCollection.GetResourceIpsAsync(); ApplyFilter(); } private string Filter { get => _filter; set { if (_filter == value) return; _filter = value; ApplyFilter(); } } private void ApplyFilter() { var filter = _filter?.Trim(); IEnumerable<(Resource resource, string ip)> filtered = _all; if (!string.IsNullOrWhiteSpace(filter)) { // PORT MODE (":22" or ":2" etc.) — prefix match as user types if (filter.StartsWith(":")) { var portText = filter[1..].Trim(); if (string.IsNullOrEmpty(portText)) { // ":" alone -> show everything with a port filtered = _all.Where(x => x.resource is Service { Network.Port: not null }); } else if (portText.All(char.IsDigit)) { filtered = _all.Where(x => x.resource is Service { Network.Port: not null } service && service.Network!.Port!.Value.ToString().StartsWith(portText, StringComparison.Ordinal)); } else { // ":abc" -> no matches filtered = []; } } else { // IP OR PORT MATCH (non ":" input) filtered = _all.Where(x => x.ip.Contains(filter, StringComparison.OrdinalIgnoreCase) || ( x.resource is Service { Network.Port: not null } service && service.Network!.Port!.Value.ToString() .Contains(filter, StringComparison.Ordinal) ) ); } } _grouped = filtered .Where(x => !string.IsNullOrWhiteSpace(x.ip)) .GroupBy(x => GetSubnet(x.ip)) .ToDictionary( g => g.Key, g => g.GroupBy(x => x.ip) .ToDictionary( ip => ip.Key, ip => ip.ToList() ) ); StateHasChanged(); } private string GetSubnet(string ip) { var parts = ip.Split('.'); return parts.Length == 4 ? $"{parts[0]}.{parts[1]}.{parts[2]}.x" : "unknown"; } private string GetResourceUrl(Resource resource) { return resource switch { SystemResource => $"resources/systems/{Uri.EscapeDataString(resource.Name)}", Service => $"resources/services/{Uri.EscapeDataString(resource.Name)}", _ => "#" }; } private string GetNextFreeIp() { if (_grouped is null || !_grouped.Any()) return "n/a"; foreach (var subnet in _grouped.OrderBy(x => x.Key)) { var usedHosts = subnet.Value.Keys .Select(ip => ip.Split('.').Last()) .Select(part => int.TryParse(part, out var n) ? n : -1) .Where(n => n >= 0) .ToHashSet(); for (var host = 1; host < 255; host++) { if (!usedHosts.Contains(host)) { var baseSubnet = subnet.Key.Replace(".x", ""); return $"{baseSubnet}.{host}"; } } } return "full"; } private int GetNextFreePort() { if (_grouped is null) return 1024; var usedPorts = _grouped .SelectMany(s => s.Value) .SelectMany(ip => ip.Value) .Select(x => x.Item1) .OfType() .Where(s => s.Network?.Port is not null) .Select(s => s.Network!.Port!.Value) .ToHashSet(); for (var port = 1024; port < 65535; port++) { if (!usedPorts.Contains(port)) return port; } return -1; } private static uint IpToSortable(string ip) { var parts = ip.Split('.') .Select(p => byte.TryParse(p, out var b) ? b : (byte)0) .ToArray(); if (parts.Length != 4) return 0; return ((uint)parts[0] << 24) | ((uint)parts[1] << 16) | ((uint)parts[2] << 8) | parts[3]; } }