diff --git a/MoonlightServers.ApiServer/Database/Entities/Node.cs b/MoonlightServers.ApiServer/Database/Entities/Node.cs index c63aac6..3ac6915 100644 --- a/MoonlightServers.ApiServer/Database/Entities/Node.cs +++ b/MoonlightServers.ApiServer/Database/Entities/Node.cs @@ -16,6 +16,7 @@ public class Node public string Token { get; set; } public int HttpPort { get; set; } public int FtpPort { get; set; } + public bool UseSsl { get; set; } // Misc public bool EnableTransparentMode { get; set; } diff --git a/MoonlightServers.ApiServer/Database/Migrations/20241213181416_AddedNodeSslField.Designer.cs b/MoonlightServers.ApiServer/Database/Migrations/20241213181416_AddedNodeSslField.Designer.cs new file mode 100644 index 0000000..8d0f772 --- /dev/null +++ b/MoonlightServers.ApiServer/Database/Migrations/20241213181416_AddedNodeSslField.Designer.cs @@ -0,0 +1,437 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MoonlightServers.ApiServer.Database; + +#nullable disable + +namespace MoonlightServers.ApiServer.Database.Migrations +{ + [DbContext(typeof(ServersDataContext))] + [Migration("20241213181416_AddedNodeSslField")] + partial class AddedNodeSslField + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Servers") + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Allocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("NodeId"); + + b.HasIndex("ServerId"); + + b.ToTable("Allocations", "Servers"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("EnableDynamicFirewall") + .HasColumnType("tinyint(1)"); + + b.Property("EnableTransparentMode") + .HasColumnType("tinyint(1)"); + + b.Property("Fqdn") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FtpPort") + .HasColumnType("int"); + + b.Property("HttpPort") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Token") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UseSsl") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Nodes", "Servers"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Bandwidth") + .HasColumnType("int"); + + b.Property("Cpu") + .HasColumnType("int"); + + b.Property("Disk") + .HasColumnType("int"); + + b.Property("DockerImageIndex") + .HasColumnType("int"); + + b.Property("Memory") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("StarId") + .HasColumnType("int"); + + b.Property("StartupOverride") + .HasColumnType("longtext"); + + b.Property("UseVirtualDisk") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("NodeId"); + + b.HasIndex("StarId"); + + b.ToTable("Servers", "Servers"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerBackup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Completed") + .HasColumnType("tinyint(1)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Successful") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("ServerBackups", "Servers"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerVariables", "Servers"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AllowDockerImageChange") + .HasColumnType("tinyint(1)"); + + b.Property("Author") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DefaultDockerImage") + .HasColumnType("int"); + + b.Property("DonateUrl") + .HasColumnType("longtext"); + + b.Property("InstallDockerImage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallScript") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallShell") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OnlineDetection") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ParseConfiguration") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RequiredAllocations") + .HasColumnType("int"); + + b.Property("StartupCommand") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StopCommand") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdateUrl") + .HasColumnType("longtext"); + + b.Property("Version") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Stars", "Servers"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarDockerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AutoPulling") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StarId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("StarId"); + + b.ToTable("StarDockerImages", "Servers"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AllowEditing") + .HasColumnType("tinyint(1)"); + + b.Property("AllowViewing") + .HasColumnType("tinyint(1)"); + + b.Property("DefaultValue") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Filter") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StarId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("StarId"); + + b.ToTable("StarVariables", "Servers"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Allocation", b => + { + b.HasOne("MoonlightServers.ApiServer.Database.Entities.Node", "Node") + .WithMany("Allocations") + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server") + .WithMany("Allocations") + .HasForeignKey("ServerId"); + + b.Navigation("Node"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b => + { + b.HasOne("MoonlightServers.ApiServer.Database.Entities.Node", "Node") + .WithMany("Servers") + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star") + .WithMany() + .HasForeignKey("StarId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Node"); + + b.Navigation("Star"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b => + { + b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarDockerImage", b => + { + b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star") + .WithMany("DockerImages") + .HasForeignKey("StarId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Star"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarVariable", b => + { + b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star") + .WithMany("Variables") + .HasForeignKey("StarId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Star"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b => + { + b.Navigation("Allocations"); + + b.Navigation("Servers"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b => + { + b.Navigation("Allocations"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b => + { + b.Navigation("DockerImages"); + + b.Navigation("Variables"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MoonlightServers.ApiServer/Database/Migrations/20241213181416_AddedNodeSslField.cs b/MoonlightServers.ApiServer/Database/Migrations/20241213181416_AddedNodeSslField.cs new file mode 100644 index 0000000..db77984 --- /dev/null +++ b/MoonlightServers.ApiServer/Database/Migrations/20241213181416_AddedNodeSslField.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MoonlightServers.ApiServer.Database.Migrations +{ + /// + public partial class AddedNodeSslField : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UseSsl", + schema: "Servers", + table: "Nodes", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UseSsl", + schema: "Servers", + table: "Nodes"); + } + } +} diff --git a/MoonlightServers.ApiServer/Database/Migrations/ServersDataContextModelSnapshot.cs b/MoonlightServers.ApiServer/Database/Migrations/ServersDataContextModelSnapshot.cs index ce53445..c1fb055 100644 --- a/MoonlightServers.ApiServer/Database/Migrations/ServersDataContextModelSnapshot.cs +++ b/MoonlightServers.ApiServer/Database/Migrations/ServersDataContextModelSnapshot.cs @@ -85,6 +85,9 @@ namespace MoonlightServers.ApiServer.Database.Migrations .IsRequired() .HasColumnType("longtext"); + b.Property("UseSsl") + .HasColumnType("tinyint(1)"); + b.HasKey("Id"); b.ToTable("Nodes", "Servers"); diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodeSystemController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodeSystemController.cs new file mode 100644 index 0000000..60c34d1 --- /dev/null +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodeSystemController.cs @@ -0,0 +1,78 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using MoonCore.Exceptions; +using MoonCore.Extended.Abstractions; +using MoonCore.Helpers; +using MoonlightServers.ApiServer.Database.Entities; +using MoonlightServers.ApiServer.Services; +using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Sys; + +namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Nodes; + +[ApiController] +[Route("api/admin/servers/nodes")] +public class NodeSystemController : Controller +{ + private readonly DatabaseRepository NodeRepository; + private readonly NodeService NodeService; + + public NodeSystemController(DatabaseRepository nodeRepository, NodeService nodeService) + { + NodeRepository = nodeRepository; + NodeService = nodeService; + } + + [HttpGet("{nodeId}/system/status")] + public async Task GetStatus([FromRoute] int nodeId) + { + var node = GetNode(nodeId); + + NodeSystemStatusResponse response; + + var sw = new Stopwatch(); + sw.Start(); + + try + { + var statusResponse = await NodeService.GetSystemStatus(node); + + sw.Stop(); + + response = new() + { + Version = statusResponse.Version, + RoundtripError = statusResponse.TripError, + RoundtripSuccess = statusResponse.TripSuccess, + RoundtripTime = statusResponse.TripTime + sw.Elapsed, + RoundtripRemoteFailure = !statusResponse.TripSuccess // When the remote trip failed, it's the remotes fault + }; + } + catch (Exception e) + { + sw.Stop(); + + response = new() + { + Version = "Unknown", + RoundtripError = e.Message, + RoundtripSuccess = false, + RoundtripTime = sw.Elapsed, + RoundtripRemoteFailure = false + }; + } + + return response; + } + + private Node GetNode(int nodeId) + { + var result = NodeRepository + .Get() + .FirstOrDefault(x => x.Id == nodeId); + + if (result == null) + throw new HttpApiException("A node with this id could not be found", 404); + + return result; + } +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Http/Controllers/Remote/Node/NodeTripController.cs b/MoonlightServers.ApiServer/Http/Controllers/Remote/Node/NodeTripController.cs new file mode 100644 index 0000000..c0fd583 --- /dev/null +++ b/MoonlightServers.ApiServer/Http/Controllers/Remote/Node/NodeTripController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace MoonlightServers.ApiServer.Http.Controllers.Remote.Node; + +[ApiController] +[Route("api/servers/remote/node")] +public class NodeTripController : Controller +{ + [HttpGet("trip")] + public Task Get() => Task.CompletedTask; +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj b/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj index 4617d4f..eb0151c 100644 --- a/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj +++ b/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj @@ -13,6 +13,7 @@ + diff --git a/MoonlightServers.ApiServer/Services/NodeService.cs b/MoonlightServers.ApiServer/Services/NodeService.cs new file mode 100644 index 0000000..ab0bbe8 --- /dev/null +++ b/MoonlightServers.ApiServer/Services/NodeService.cs @@ -0,0 +1,52 @@ +using MoonCore.Attributes; +using MoonCore.Helpers; +using MoonlightServers.ApiServer.Database.Entities; +using MoonlightServers.DaemonShared.Http.Responses.Sys; + +namespace MoonlightServers.ApiServer.Services; + +[Singleton] +public class NodeService +{ + public async Task CreateApiClient(Node node) + { + string url = ""; + + if (node.UseSsl) + url += "https://"; + else + url += "http://"; + + url += $"{node.Fqdn}:{node.HttpPort}/"; + + var httpClient = new HttpClient() + { + BaseAddress = new Uri(url) + }; + + httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {node.Token}"); + + return new HttpApiClient(httpClient); + } + + public async Task GetSystemInfo(Node node) + { + using var apiClient = await CreateApiClient(node); + + return await apiClient.GetJson("api/system/info"); + } + + public async Task GetSystemStatus(Node node) + { + using var apiClient = await CreateApiClient(node); + + return await apiClient.GetJson("api/system/status"); + } + + public async Task GetSystemDataUsage(Node node) + { + using var apiClient = await CreateApiClient(node); + + return await apiClient.GetJson("api/system/dataUsage"); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Configuration/AppConfiguration.cs b/MoonlightServers.Daemon/Configuration/AppConfiguration.cs index f60e255..096693f 100644 --- a/MoonlightServers.Daemon/Configuration/AppConfiguration.cs +++ b/MoonlightServers.Daemon/Configuration/AppConfiguration.cs @@ -5,6 +5,12 @@ public class AppConfiguration public DockerData Docker { get; set; } = new(); public StorageData Storage { get; set; } = new(); public SecurityData Security { get; set; } = new(); + public RemoteData Remote { get; set; } = new(); + + public class RemoteData + { + public string Url { get; set; } + } public class DockerData { diff --git a/MoonlightServers.Daemon/Http/Controllers/Sys/SystemStatusController.cs b/MoonlightServers.Daemon/Http/Controllers/Sys/SystemStatusController.cs new file mode 100644 index 0000000..a9587c9 --- /dev/null +++ b/MoonlightServers.Daemon/Http/Controllers/Sys/SystemStatusController.cs @@ -0,0 +1,54 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using MoonlightServers.Daemon.Services; +using MoonlightServers.DaemonShared.Http.Responses.Sys; + +namespace MoonlightServers.Daemon.Http.Controllers.Sys; + +[ApiController] +[Route("api/system/status")] +public class SystemStatusController : Controller +{ + private readonly RemoteService RemoteService; + + public SystemStatusController(RemoteService remoteService) + { + RemoteService = remoteService; + } + + public async Task Get() + { + SystemStatusResponse response; + + var sw = new Stopwatch(); + sw.Start(); + + try + { + await RemoteService.GetStatus(); + + sw.Stop(); + + response = new() + { + TripSuccess = true, + TripTime = sw.Elapsed, + Version = "2.1.0" // TODO: Set global + }; + } + catch (Exception e) + { + sw.Stop(); + + response = new() + { + TripError = e.Message, + TripTime = sw.Elapsed, + TripSuccess = false, + Version = "2.1.0" // TODO: Set global + }; + } + + return response; + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Services/RemoteService.cs b/MoonlightServers.Daemon/Services/RemoteService.cs new file mode 100644 index 0000000..89da2bb --- /dev/null +++ b/MoonlightServers.Daemon/Services/RemoteService.cs @@ -0,0 +1,40 @@ +using MoonCore.Attributes; +using MoonCore.Helpers; +using MoonlightServers.Daemon.Configuration; + +namespace MoonlightServers.Daemon.Services; + +[Singleton] +public class RemoteService +{ + private readonly AppConfiguration Configuration; + + public RemoteService(AppConfiguration configuration) + { + Configuration = configuration; + } + + public Task CreateHttpClient() + { + var formattedUrl = Configuration.Remote.Url.EndsWith('/') + ? Configuration.Remote.Url + : Configuration.Remote.Url + "/"; + + var httpClient = new HttpClient() + { + BaseAddress = new Uri(formattedUrl) + }; + + httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {Configuration.Security.Token}"); + + var apiClient = new HttpApiClient(httpClient); + + return Task.FromResult(apiClient); + } + + public async Task GetStatus() + { + using var apiClient = await CreateHttpClient(); + await apiClient.Get("api/servers/remote/node/trip"); + } +} \ No newline at end of file diff --git a/MoonlightServers.DaemonShared/Http/Responses/Sys/SystemStatusResponse.cs b/MoonlightServers.DaemonShared/Http/Responses/Sys/SystemStatusResponse.cs new file mode 100644 index 0000000..87feef4 --- /dev/null +++ b/MoonlightServers.DaemonShared/Http/Responses/Sys/SystemStatusResponse.cs @@ -0,0 +1,9 @@ +namespace MoonlightServers.DaemonShared.Http.Responses.Sys; + +public class SystemStatusResponse +{ + public bool TripSuccess { get; set; } + public TimeSpan TripTime { get; set; } + public string? TripError { get; set; } + public string Version { get; set; } +} \ No newline at end of file diff --git a/MoonlightServers.Frontend/Services/NodeService.cs b/MoonlightServers.Frontend/Services/NodeService.cs new file mode 100644 index 0000000..619284c --- /dev/null +++ b/MoonlightServers.Frontend/Services/NodeService.cs @@ -0,0 +1,21 @@ +using MoonCore.Attributes; +using MoonCore.Helpers; +using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Sys; + +namespace MoonlightServers.Frontend.Services; + +[Scoped] +public class NodeService +{ + private readonly HttpApiClient HttpApiClient; + + public NodeService(HttpApiClient httpApiClient) + { + HttpApiClient = httpApiClient; + } + + public async Task GetSystemStatus(int nodeId) + { + return await HttpApiClient.GetJson($"api/admin/servers/nodes/{nodeId}/system/status"); + } +} \ No newline at end of file diff --git a/MoonlightServers.Frontend/Startup/PluginStartup.cs b/MoonlightServers.Frontend/Startup/PluginStartup.cs new file mode 100644 index 0000000..5db24ce --- /dev/null +++ b/MoonlightServers.Frontend/Startup/PluginStartup.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using MoonCore.Extensions; +using Moonlight.Client.Interfaces; + +namespace MoonlightServers.Frontend.Startup; + +public class PluginStartup : IAppStartup +{ + public Task BuildApp(WebAssemblyHostBuilder builder) + { + builder.Services.AutoAddServices(); + + return Task.CompletedTask; + } + + public Task ConfigureApp(WebAssemblyHost app) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/MoonlightServers.Frontend/UI/Views/Admin/Nodes/Index.razor b/MoonlightServers.Frontend/UI/Views/Admin/Nodes/Index.razor index 3de409e..47d23f4 100644 --- a/MoonlightServers.Frontend/UI/Views/Admin/Nodes/Index.razor +++ b/MoonlightServers.Frontend/UI/Views/Admin/Nodes/Index.razor @@ -1,13 +1,19 @@ @page "/admin/servers/nodes" +@using System.Diagnostics +@using MoonCore.Blazor.Tailwind.Alerts @using MoonCore.Blazor.Tailwind.MinimalCrud @using MoonCore.Helpers @using MoonCore.Models @using MoonCore.Blazor.Tailwind.DataTable @using MoonlightServers.Shared.Http.Responses.Admin.Nodes @using MoonCore.Blazor.Tailwind.Components +@using MoonlightServers.Frontend.Services +@using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Sys @inject HttpApiClient ApiClient +@inject NodeService NodeService +@inject AlertService AlertService
@@ -20,10 +26,56 @@ @code - - - { + private Dictionary Responses = new(); + + private Task LoadNodeStatus(int node) + { + Task.Run(async () => + { + try + { + var status = await NodeService.GetSystemStatus(node); + + lock (Responses) + Responses[node] = status; + } + catch (Exception e) + { + lock (Responses) + Responses[node] = null; + } + + await InvokeAsync(StateHasChanged); + }); + + return Task.CompletedTask; + } + + private async Task ShowErrorDetails(int id) + { + NodeSystemStatusResponse? data; + + lock (Responses) + data = Responses.GetValueOrDefault(id); + + if (data == null) + return; + + var message = $"Failed after {Math.Round(data.RoundtripTime.TotalSeconds, 2)} seconds: " + + (data.RoundtripRemoteFailure ? "(Failed at node)" : "(Failed at api server)") + + $" {data.RoundtripError}"; + + await AlertService.Danger("Node error details", message); + } + private void OnConfigure(MinimalCrudOptions options) { options.Title = "Nodes"; diff --git a/MoonlightServers.Shared/Http/Responses/Admin/Nodes/Sys/NodeSystemStatusResponse.cs b/MoonlightServers.Shared/Http/Responses/Admin/Nodes/Sys/NodeSystemStatusResponse.cs new file mode 100644 index 0000000..1984b38 --- /dev/null +++ b/MoonlightServers.Shared/Http/Responses/Admin/Nodes/Sys/NodeSystemStatusResponse.cs @@ -0,0 +1,10 @@ +namespace MoonlightServers.Shared.Http.Responses.Admin.Nodes.Sys; + +public class NodeSystemStatusResponse +{ + public bool RoundtripSuccess { get; set; } + public bool RoundtripRemoteFailure { get; set; } + public TimeSpan RoundtripTime { get; set; } + public string? RoundtripError { get; set; } + public string Version { get; set; } +} \ No newline at end of file