diff --git a/Moonlight/App/Helpers/Formatter.cs b/Moonlight/App/Helpers/Formatter.cs index 132a6fd9..05faba8c 100644 --- a/Moonlight/App/Helpers/Formatter.cs +++ b/Moonlight/App/Helpers/Formatter.cs @@ -156,6 +156,32 @@ public static class Formatter return (i / (1024D * 1024D)).Round(2) + " GB"; } } + + public static double CalculateAverage(List values) + { + if (values == null || values.Count == 0) + { + throw new ArgumentException("The list cannot be null or empty."); + } + + double sum = 0; + foreach (double value in values) + { + sum += value; + } + + return sum / values.Count; + } + + public static double CalculatePercentage(double part, double total) + { + if (total == 0) + { + return 0; + } + + return (part / total) * 100; + } public static RenderFragment FormatLineBreaks(string content) { diff --git a/Moonlight/App/Perms/Permissions.cs b/Moonlight/App/Perms/Permissions.cs index bc74ca82..cb6f4dab 100644 --- a/Moonlight/App/Perms/Permissions.cs +++ b/Moonlight/App/Perms/Permissions.cs @@ -410,10 +410,17 @@ public static class Permissions public static Permission AdminChangelog = new() { - Index = 59, + Index = 60, Name = "Admin changelog", Description = "View the changelog" }; + + public static Permission AdminStatisticsLive = new() + { + Index = 61, + Name = "Admin statistics live", + Description = "View the live statistics" + }; public static Permission? FromString(string name) { diff --git a/Moonlight/Pages/_Layout.cshtml b/Moonlight/Pages/_Layout.cshtml index c840ed2c..42e0443e 100644 --- a/Moonlight/Pages/_Layout.cshtml +++ b/Moonlight/Pages/_Layout.cshtml @@ -49,9 +49,7 @@ - - - + @@ -99,6 +97,9 @@ + + + @@ -122,9 +123,6 @@ moonlight.loading.registerXterm(); - - - diff --git a/Moonlight/Shared/Views/Admin/Statistics.razor b/Moonlight/Shared/Views/Admin/Statistics/Index.razor similarity index 100% rename from Moonlight/Shared/Views/Admin/Statistics.razor rename to Moonlight/Shared/Views/Admin/Statistics/Index.razor diff --git a/Moonlight/Shared/Views/Admin/Statistics/Live.razor b/Moonlight/Shared/Views/Admin/Statistics/Live.razor new file mode 100644 index 00000000..fc3e9a04 --- /dev/null +++ b/Moonlight/Shared/Views/Admin/Statistics/Live.razor @@ -0,0 +1,201 @@ +@page "/admin/statistics/live" + +@using Moonlight.App.Services +@using Moonlight.App.Repositories +@using Moonlight.App.Database.Entities +@using Moonlight.App.Helpers +@using Moonlight.App.Services.Sessions + +@inject NodeService NodeService +@inject Repository NodeRepository +@inject IServiceScopeFactory ServiceScopeFactory + +@attribute [PermissionRequired(nameof(Permissions.AdminStatisticsLive))] + +
+
+
+
+
+ @(Math.Round(TotalCpuUsed, 2))% / 100% + + Total cpu load + +
+
+
+
+ @{ + var cpuPercent = Math.Round(Formatter.CalculatePercentage(TotalCpuUsed, 100)); + } + +
+ @(cpuPercent)% +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ @(ByteSizeValue.FromKiloBytes(TotalMemoryUsed).GigaBytes)GB / @(ByteSizeValue.FromKiloBytes(TotalMemory).GigaBytes)GB + + Total memory load + +
+
+
+
+ @{ + var memoryPercent = Math.Round(Formatter.CalculatePercentage(TotalMemoryUsed, TotalMemory)); + } + +
+ @(memoryPercent)% +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ @(Users) + + Total user count + +
+
+
+
+ +
+
+
+
+ @(Sessions) + + Total session count + +
+
+
+
+ +
+
+
+
+ @(ActiveUsers) + + Total active user count + +
+
+
+
+
+ +@code +{ + private long TotalMemoryUsed; + private long TotalMemory; + + private double TotalCpuUsed; + + private int Users; + private int ActiveUsers; + private int Sessions; + + protected override Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + Task.Run(async () => + { + while (true) + { + await Monitor(); + await Task.Delay(TimeSpan.FromSeconds(5)); + } + }); + } + + return Task.CompletedTask; + } + + private async Task Monitor() + { + async Task Nodes() + { + TotalMemory = 0; + TotalMemoryUsed = 0; + + var cpuValues = new List(); + + foreach (var node in NodeRepository.Get().ToArray()) + { + try + { + var metrics = await NodeService.GetMemoryMetrics(node); + + TotalMemory += metrics.Total; + TotalMemoryUsed += metrics.Used; + + var cpuMetrics = await NodeService.GetCpuMetrics(node); + cpuValues.Add(cpuMetrics.CpuUsage); + } + catch (Exception) + { + // ignored + } + } + + TotalCpuUsed = Formatter.CalculateAverage(cpuValues); + + await InvokeAsync(StateHasChanged); + } + + async Task UsersAndSessions() + { + using var scope = ServiceScopeFactory.CreateScope(); + + var userRepo = scope.ServiceProvider.GetRequiredService>(); + var sessionService = scope.ServiceProvider.GetRequiredService(); + + Users = userRepo.Get().Count(); + Sessions = (await sessionService.GetSessions()).Length; + ActiveUsers = userRepo + .Get() + .Count(x => x.LastVisitedAt > DateTime.UtcNow.AddDays(-1)); + + await InvokeAsync(StateHasChanged); + } + + await Nodes(); + await UsersAndSessions(); + } + + private string GetStateColor(double percent) + { + if (percent < 60) + return "success"; + else if (percent >= 60 && percent < 80) + return "warning"; + else + return "danger"; + } +} \ No newline at end of file