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 Microsoft.Extensions.Logging.Console;
|
||||||
using Moonlight.Shared.Http;
|
using Moonlight.Shared.Http;
|
||||||
using Moonlight.Api.Helpers;
|
using Moonlight.Api.Helpers;
|
||||||
|
using Moonlight.Api.Services;
|
||||||
|
|
||||||
namespace Moonlight.Api.Startup;
|
namespace Moonlight.Api.Startup;
|
||||||
|
|
||||||
@@ -19,6 +20,9 @@ public partial class Startup
|
|||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); });
|
builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); });
|
||||||
builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>();
|
builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>();
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<ApplicationService>();
|
||||||
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<ApplicationService>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UseBase(WebApplication application)
|
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
|
IsExactPath = true
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
|
{
|
||||||
|
Name = "Overview",
|
||||||
|
IconType = typeof(LayoutDashboardIcon),
|
||||||
|
Path = "/admin",
|
||||||
|
IsExactPath = true,
|
||||||
|
Group = "Admin"
|
||||||
|
},
|
||||||
|
new()
|
||||||
{
|
{
|
||||||
Name = "Users",
|
Name = "Users",
|
||||||
IconType = typeof(UsersRoundIcon),
|
IconType = typeof(UsersRoundIcon),
|
||||||
@@ -87,6 +95,14 @@
|
|||||||
IsExactPath = false,
|
IsExactPath = false,
|
||||||
Group = "Admin"
|
Group = "Admin"
|
||||||
},
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Settings",
|
||||||
|
IconType = typeof(SettingsIcon),
|
||||||
|
Path = "/admin/settings",
|
||||||
|
IsExactPath = false,
|
||||||
|
Group = "Admin"
|
||||||
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Navigation.LocationChanged += OnLocationChanged;
|
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 System.Text.Json.Serialization;
|
||||||
using Moonlight.Shared.Http.Requests.Users;
|
using Moonlight.Shared.Http.Requests.Users;
|
||||||
using Moonlight.Shared.Http.Responses;
|
using Moonlight.Shared.Http.Responses;
|
||||||
|
using Moonlight.Shared.Http.Responses.Admin;
|
||||||
using Moonlight.Shared.Http.Responses.Auth;
|
using Moonlight.Shared.Http.Responses.Auth;
|
||||||
using Moonlight.Shared.Http.Responses.Users;
|
using Moonlight.Shared.Http.Responses.Users;
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ namespace Moonlight.Shared.Http;
|
|||||||
[JsonSerializable(typeof(ClaimResponse[]))]
|
[JsonSerializable(typeof(ClaimResponse[]))]
|
||||||
[JsonSerializable(typeof(SchemeResponse[]))]
|
[JsonSerializable(typeof(SchemeResponse[]))]
|
||||||
[JsonSerializable(typeof(UserResponse))]
|
[JsonSerializable(typeof(UserResponse))]
|
||||||
|
[JsonSerializable(typeof(SystemInfoResponse))]
|
||||||
[JsonSerializable(typeof(PagedData<UserResponse>))]
|
[JsonSerializable(typeof(PagedData<UserResponse>))]
|
||||||
public partial class SerializationContext : JsonSerializerContext
|
public partial class SerializationContext : JsonSerializerContext
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user