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 @@
-
-
- Online (v.2.1.0)
-
+
+ @{
+ bool isFetched;
+ NodeSystemStatusResponse? data;
+
+ lock (Responses)
+ isFetched = Responses.TryGetValue(context.Id, out data);
+ }
+
+ @if (isFetched)
+ {
+ if (data == null)
+ {
+
+
+
+ API Error
+
+
+ }
+ else
+ {
+ if (data.RoundtripSuccess)
+ {
+
+
+ Online (@(data.Version))
+
+ }
+ else
+ {
+
+
+
+ Error
+ ShowErrorDetails(context.Id)" @onclick:preventDefault
+ href="#" class="ms-1 text-gray-600">Details
+
+
+ }
+ }
+ }
+ else
+ {
+
+
+ Loading
+
+ }
+
@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