Added small system overview
This commit is contained in:
98
Moonlight.Api/Helpers/OsHelper.cs
Normal file
98
Moonlight.Api/Helpers/OsHelper.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Moonlight.Api.Helpers;
|
||||
|
||||
public class OsHelper
|
||||
{
|
||||
public static string GetName()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
return GetWindowsVersion();
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
return GetLinuxVersion();
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
return "Goofy OS";
|
||||
|
||||
return "Unknown OS";
|
||||
}
|
||||
|
||||
private static string GetWindowsVersion()
|
||||
{
|
||||
var version = Environment.OSVersion.Version;
|
||||
|
||||
// Windows 11 is version 10.0 build 22000+
|
||||
if (version.Major == 10 && version.Build >= 22000)
|
||||
return $"Windows 11 ({version.Build})";
|
||||
|
||||
if (version.Major == 10)
|
||||
return $"Windows 10 ({version.Build})";
|
||||
|
||||
if (version.Major == 6 && version.Minor == 3)
|
||||
return "Windows 8.1";
|
||||
|
||||
if (version.Major == 6 && version.Minor == 2)
|
||||
return "Windows 8";
|
||||
|
||||
if (version.Major == 6 && version.Minor == 1)
|
||||
return "Windows 7";
|
||||
|
||||
return $"Windows {version.Major}.{version.Minor}";
|
||||
}
|
||||
|
||||
private static string GetLinuxVersion()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Read /etc/os-release, should work everywhere
|
||||
if (File.Exists("/etc/os-release"))
|
||||
{
|
||||
var lines = File.ReadAllLines("/etc/os-release");
|
||||
string? name = null;
|
||||
string? version = null;
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.StartsWith("NAME="))
|
||||
name = line.Substring(5).Trim('"');
|
||||
else if (line.StartsWith("VERSION_ID="))
|
||||
version = line.Substring(11).Trim('"');
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
return string.IsNullOrEmpty(version) ? name : $"{name} {version}";
|
||||
}
|
||||
}
|
||||
|
||||
//If for some weird reason it still uses lsb release
|
||||
if (File.Exists("/etc/lsb-release"))
|
||||
{
|
||||
var lines = File.ReadAllLines("/etc/lsb-release");
|
||||
string? name = null;
|
||||
string? version = null;
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.StartsWith("DISTRIB_ID="))
|
||||
name = line.Substring(11);
|
||||
else if (line.StartsWith("DISTRIB_RELEASE="))
|
||||
version = line.Substring(16);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
return string.IsNullOrEmpty(version) ? name : $"{name} {version}";
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return $"Linux {Environment.OSVersion.Version}";
|
||||
}
|
||||
}
|
||||
33
Moonlight.Api/Http/Controllers/Admin/SystemController.cs
Normal file
33
Moonlight.Api/Http/Controllers/Admin/SystemController.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moonlight.Api.Services;
|
||||
using Moonlight.Shared.Http.Responses.Admin;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/system")]
|
||||
public class SystemController : Controller
|
||||
{
|
||||
private readonly ApplicationService ApplicationService;
|
||||
|
||||
public SystemController(ApplicationService applicationService)
|
||||
{
|
||||
ApplicationService = applicationService;
|
||||
}
|
||||
|
||||
[HttpGet("info")]
|
||||
public async Task<ActionResult<SystemInfoResponse>> GetInfoAsync()
|
||||
{
|
||||
var cpuUsage = await ApplicationService.GetCpuUsageAsync();
|
||||
var memoryUsage = await ApplicationService.GetMemoryUsageAsync();
|
||||
|
||||
return new SystemInfoResponse(
|
||||
cpuUsage,
|
||||
memoryUsage,
|
||||
ApplicationService.OperatingSystem,
|
||||
DateTimeOffset.UtcNow - ApplicationService.StartedAt,
|
||||
ApplicationService.VersionName,
|
||||
ApplicationService.IsUpToDate
|
||||
);
|
||||
}
|
||||
}
|
||||
64
Moonlight.Api/Services/ApplicationService.cs
Normal file
64
Moonlight.Api/Services/ApplicationService.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Moonlight.Api.Helpers;
|
||||
|
||||
namespace Moonlight.Api.Services;
|
||||
|
||||
public class ApplicationService : IHostedLifecycleService
|
||||
{
|
||||
public DateTimeOffset StartedAt { get; private set; }
|
||||
public string VersionName { get; private set; } = "N/A";
|
||||
public bool IsUpToDate { get; set; } = true;
|
||||
public string OperatingSystem { get; private set; } = "N/A";
|
||||
|
||||
public Task<long> GetMemoryUsageAsync()
|
||||
{
|
||||
using var currentProcess = Process.GetCurrentProcess();
|
||||
return Task.FromResult(currentProcess.WorkingSet64);
|
||||
}
|
||||
|
||||
public async Task<double> GetCpuUsageAsync()
|
||||
{
|
||||
using var currentProcess = Process.GetCurrentProcess();
|
||||
|
||||
// Get initial values
|
||||
var startCpuTime = currentProcess.TotalProcessorTime;
|
||||
var startTime = DateTime.UtcNow;
|
||||
|
||||
// Wait a bit to calculate the diff
|
||||
await Task.Delay(500);
|
||||
|
||||
// New values
|
||||
var endCpuTime = currentProcess.TotalProcessorTime;
|
||||
var endTime = DateTime.UtcNow;
|
||||
|
||||
// Calculate CPU usage
|
||||
var cpuUsedMs = (endCpuTime - startCpuTime).TotalMilliseconds;
|
||||
var totalMsPassed = (endTime - startTime).TotalMilliseconds;
|
||||
var cpuUsagePercent = (cpuUsedMs / (Environment.ProcessorCount * totalMsPassed)) * 100;
|
||||
|
||||
return Math.Round(cpuUsagePercent, 2);
|
||||
}
|
||||
|
||||
public async Task StartedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
StartedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
// TODO: Update / version check
|
||||
|
||||
VersionName = "v2.1.0 (a2d4edc0e5)";
|
||||
IsUpToDate = true;
|
||||
|
||||
OperatingSystem = OsHelper.GetName();
|
||||
}
|
||||
|
||||
#region Unused
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
using Moonlight.Shared.Http;
|
||||
using Moonlight.Api.Helpers;
|
||||
using Moonlight.Api.Services;
|
||||
|
||||
namespace Moonlight.Api.Startup;
|
||||
|
||||
@@ -19,6 +20,9 @@ public partial class Startup
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); });
|
||||
builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>();
|
||||
|
||||
builder.Services.AddSingleton<ApplicationService>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ApplicationService>());
|
||||
}
|
||||
|
||||
private static void UseBase(WebApplication application)
|
||||
|
||||
46
Moonlight.Frontend/Formatter.cs
Normal file
46
Moonlight.Frontend/Formatter.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
namespace Moonlight.Frontend;
|
||||
|
||||
public static class Formatter
|
||||
{
|
||||
public static string FormatSize(long bytes, double conversionStep = 1024)
|
||||
{
|
||||
if (bytes == 0) return "0 B";
|
||||
|
||||
string[] units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
|
||||
var unitIndex = 0;
|
||||
double size = bytes;
|
||||
|
||||
while (size >= conversionStep && unitIndex < units.Length - 1)
|
||||
{
|
||||
size /= conversionStep;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
var decimals = unitIndex == 0 ? 0 : 2;
|
||||
return $"{Math.Round(size, decimals)} {units[unitIndex]}";
|
||||
}
|
||||
|
||||
public static string FormatDuration(TimeSpan timeSpan)
|
||||
{
|
||||
var abs = timeSpan.Duration(); // Handle negative timespans
|
||||
|
||||
if (abs.TotalSeconds < 1)
|
||||
return $"{abs.TotalMilliseconds:F0}ms";
|
||||
|
||||
if (abs.TotalMinutes < 1)
|
||||
return $"{abs.TotalSeconds:F1}s";
|
||||
|
||||
if (abs.TotalHours < 1)
|
||||
return $"{abs.Minutes}m {abs.Seconds}s";
|
||||
|
||||
if (abs.TotalDays < 1)
|
||||
return $"{abs.Hours}h {abs.Minutes}m";
|
||||
|
||||
if (abs.TotalDays < 365)
|
||||
return $"{abs.Days}d {abs.Hours}h";
|
||||
|
||||
var years = (int)(abs.TotalDays / 365);
|
||||
var days = abs.Days % 365;
|
||||
return days > 0 ? $"{years}y {days}d" : $"{years}y";
|
||||
}
|
||||
}
|
||||
156
Moonlight.Frontend/UI/Admin/Views/Overview.razor
Normal file
156
Moonlight.Frontend/UI/Admin/Views/Overview.razor
Normal file
@@ -0,0 +1,156 @@
|
||||
@page "/admin"
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Http.Responses.Admin
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Spinners
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
|
||||
<h1 class="text-xl font-semibold">Overview</h1>
|
||||
<div class="text-muted-foreground">
|
||||
Here you can see a quick overview of your moonlight instance
|
||||
</div>
|
||||
|
||||
<div class="mt-8 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-5">
|
||||
<Card ClassName="col-span-1">
|
||||
@if (IsInfoLoading || InfoResponse == null)
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardHeader>
|
||||
<CardDescription>CPU Usage</CardDescription>
|
||||
<CardTitle ClassName="text-lg">@Math.Round(InfoResponse.CpuUsage, 2)%</CardTitle>
|
||||
<CardAction>
|
||||
<CpuIcon ClassName="size-6 text-muted-foreground" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
@if (IsInfoLoading || InfoResponse == null)
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardHeader>
|
||||
<CardDescription>Memory Usage</CardDescription>
|
||||
<CardTitle ClassName="text-lg">@Formatter.FormatSize(InfoResponse.MemoryUsage)</CardTitle>
|
||||
<CardAction>
|
||||
<MemoryStickIcon ClassName="size-6 text-muted-foreground" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
@if (IsInfoLoading || InfoResponse == null)
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardHeader>
|
||||
<CardDescription>Operating System</CardDescription>
|
||||
<CardTitle ClassName="text-lg">@InfoResponse.OperatingSystem</CardTitle>
|
||||
<CardAction>
|
||||
<ComputerIcon ClassName="size-6 text-muted-foreground" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
@if (IsInfoLoading || InfoResponse == null)
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardHeader>
|
||||
<CardDescription>Uptime</CardDescription>
|
||||
<CardTitle ClassName="text-lg">@Formatter.FormatDuration(InfoResponse.Uptime)</CardTitle>
|
||||
<CardAction>
|
||||
<ClockIcon ClassName="size-6 text-muted-foreground" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
@if (IsInfoLoading || InfoResponse == null)
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardHeader>
|
||||
<CardDescription>Version</CardDescription>
|
||||
<CardTitle ClassName="text-lg">@InfoResponse.VersionName</CardTitle>
|
||||
<CardAction>
|
||||
<RocketIcon ClassName="size-6 text-muted-foreground" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
@if (IsInfoLoading || InfoResponse == null)
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardHeader>
|
||||
<CardDescription>Update Status</CardDescription>
|
||||
@if (InfoResponse.IsUpToDate)
|
||||
{
|
||||
<CardTitle ClassName="text-lg text-green-500">Up-to-date</CardTitle>
|
||||
<CardAction>
|
||||
<RefreshCwIcon ClassName="size-6 text-muted-foreground" />
|
||||
</CardAction>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardTitle ClassName="text-lg text-primary">Update available</CardTitle>
|
||||
<CardAction ClassName="self-center">
|
||||
<Button>Update</Button>
|
||||
</CardAction>
|
||||
}
|
||||
</CardHeader>
|
||||
}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private bool IsInfoLoading = true;
|
||||
private SystemInfoResponse? InfoResponse;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if(!firstRender)
|
||||
return;
|
||||
|
||||
InfoResponse = await HttpClient.GetFromJsonAsync<SystemInfoResponse>("api/admin/system/info", Constants.SerializerOptions);
|
||||
IsInfoLoading = false;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,14 @@
|
||||
IsExactPath = true
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Overview",
|
||||
IconType = typeof(LayoutDashboardIcon),
|
||||
Path = "/admin",
|
||||
IsExactPath = true,
|
||||
Group = "Admin"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Users",
|
||||
IconType = typeof(UsersRoundIcon),
|
||||
@@ -87,6 +95,14 @@
|
||||
IsExactPath = false,
|
||||
Group = "Admin"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Settings",
|
||||
IconType = typeof(SettingsIcon),
|
||||
Path = "/admin/settings",
|
||||
IsExactPath = false,
|
||||
Group = "Admin"
|
||||
}
|
||||
]);
|
||||
|
||||
Navigation.LocationChanged += OnLocationChanged;
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Moonlight.Shared.Http.Responses.Admin;
|
||||
|
||||
public record SystemInfoResponse(double CpuUsage, long MemoryUsage, string OperatingSystem, TimeSpan Uptime, string VersionName, bool IsUpToDate);
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Moonlight.Shared.Http.Requests.Users;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Http.Responses.Admin;
|
||||
using Moonlight.Shared.Http.Responses.Auth;
|
||||
using Moonlight.Shared.Http.Responses.Users;
|
||||
|
||||
@@ -11,6 +12,7 @@ namespace Moonlight.Shared.Http;
|
||||
[JsonSerializable(typeof(ClaimResponse[]))]
|
||||
[JsonSerializable(typeof(SchemeResponse[]))]
|
||||
[JsonSerializable(typeof(UserResponse))]
|
||||
[JsonSerializable(typeof(SystemInfoResponse))]
|
||||
[JsonSerializable(typeof(PagedData<UserResponse>))]
|
||||
public partial class SerializationContext : JsonSerializerContext
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user