54 Commits
v1b1 ... v1b5

Author SHA1 Message Date
Marcel Baumgartner
9a5b004e17 Merge pull request #163 from Moonlight-Panel/AddUserWebsiteDelete
Added webspace delete
2023-06-12 00:10:27 +02:00
Marcel Baumgartner
3aee059860 Added webspace delete 2023-06-12 00:10:03 +02:00
Marcel Baumgartner
3dfa7f66de Merge pull request #162 from Moonlight-Panel/AddUserDomainDelete
Added user domain delete
2023-06-11 22:10:41 +02:00
Marcel Baumgartner
c2949b4773 Added user domain delete 2023-06-11 22:10:28 +02:00
Marcel Baumgartner
c2d0ab4b1b Merge pull request #161 from Moonlight-Panel/FixServerDelete
Fixed server delete
2023-06-11 21:58:56 +02:00
Marcel Baumgartner
de02f0bd74 Fixed server delete 2023-06-11 21:50:45 +02:00
Marcel Baumgartner
e280a95619 Merge pull request #160 from Moonlight-Panel/AddVersioningSystem
Removed old app version copy instruction
2023-06-11 21:11:39 +02:00
Marcel Baumgartner
6f06be9cc6 Removed old app version copy instruction 2023-06-11 21:11:09 +02:00
Marcel Baumgartner
08745a83b4 Merge pull request #159 from Moonlight-Panel/AddVersioningSystem
Add new version and changelog system
2023-06-11 21:06:24 +02:00
Marcel Baumgartner
9a262d1396 Add new version and changelog system 2023-06-11 20:59:20 +02:00
Marcel Baumgartner
0a1b93b8fb Merge pull request #158 from Moonlight-Panel/ImproveStatistics
Added better statistics calculation and active user messurement
2023-06-11 17:57:01 +02:00
Marcel Baumgartner
4b638fc5da Added better statistics calculation and active user messurement 2023-06-11 17:56:45 +02:00
Marcel Baumgartner
d8e34ae891 Merge pull request #157 from Moonlight-Panel/FixSftpWebsitePort
Fixed website sftp port
2023-06-11 16:32:48 +02:00
Marcel Baumgartner
311237e49d Fixed website sftp port 2023-06-11 16:32:28 +02:00
Marcel Baumgartner
6591bbc927 Merge pull request #156 from Moonlight-Panel/AddServerBackgroundImage
Add dynamic background images for servers
2023-06-11 16:28:03 +02:00
Marcel Baumgartner
43c5717d19 Added default background and optimized change methods 2023-06-11 16:26:43 +02:00
Marcel Baumgartner
61d547b2ce Add dynamic background images for servers 2023-06-10 00:00:54 +02:00
Marcel Baumgartner
d7fbe54225 Merge pull request #151 from Moonlight-Panel/AddUptimeCounter
Added uptime service
2023-06-09 15:02:14 +02:00
Marcel Baumgartner
d0004e9fff Added uptime service 2023-06-09 15:01:51 +02:00
Marcel Baumgartner
829596a3e7 Merge pull request #150 from Moonlight-Panel/AddHealthChecks
Add health checks
2023-06-09 14:39:23 +02:00
Marcel Baumgartner
fc319f0f73 Added better error handling and daemon health check 2023-06-09 14:38:30 +02:00
Marcel Baumgartner
bd8ba11410 Merge pull request #149 from Moonlight-Panel/main
Update AddHealthChecks with latest commits
2023-06-09 14:21:28 +02:00
Marcel Baumgartner
0c4fc942b0 Merge pull request #148 from Moonlight-Panel/AddNewDaemonCommunication
Add new daemon communication
2023-06-07 03:34:26 +02:00
Marcel Baumgartner
94b8f07d92 Did some testing. Now able to finish new daemon communication 2023-06-07 03:29:36 +02:00
Marcel Baumgartner
f11eef2734 Merge pull request #147 from Moonlight-Panel/main
Update AddNewDaemonCommunication with latest commits
2023-06-07 02:49:53 +02:00
Marcel Baumgartner
0f8946fe27 Switched to new daemon communication 2023-06-07 02:46:26 +02:00
Marcel Baumgartner
a8cb1392e8 Merge pull request #145 from Moonlight-Panel/ImproveUserExperienceJ2S
Improved user experience for enabling and disabling join2start
2023-06-07 02:39:00 +02:00
Marcel Baumgartner
4241debc3b Improved user experience for enabling and disabling join2start 2023-06-07 02:38:21 +02:00
Marcel Baumgartner
a99959bd2b Merge pull request #144 from Moonlight-Panel/AddDeployNodeOverride
Added smart deploy node override option
2023-06-07 02:23:56 +02:00
Marcel Baumgartner
23644eb93f Added smart deploy node override option 2023-06-07 02:23:30 +02:00
Marcel Baumgartner
f8fcb86ad8 Added base health check and diagnostic system 2023-06-06 22:50:33 +02:00
Marcel Baumgartner
ce0016fa3f Merge pull request #143 from Moonlight-Panel/AddFolderDownloadHandler
Added error handler for folder download
2023-06-05 22:01:40 +02:00
Marcel Baumgartner
15d8f49ce9 Added error handler for folder download 2023-06-05 22:01:21 +02:00
Marcel Baumgartner
98d8e5b755 Merge pull request #142 from Moonlight-Panel/ImproveCpuUsageCalculation
Improved cpu usage calculation
2023-06-05 21:48:15 +02:00
Marcel Baumgartner
bfa1a09aab Improved cpu usage calculation 2023-06-05 21:47:33 +02:00
Marcel Baumgartner
84396c34e6 Merge pull request #140 from Moonlight-Panel/RemoveBundleService
Removed bundle service
2023-06-05 21:34:57 +02:00
Marcel Baumgartner
4fb4a2415b Removed bundle service 2023-06-05 21:34:09 +02:00
Marcel Baumgartner
c6cf11626e Merge pull request #139 from Moonlight-Panel/ImproveConsoleStreaming
Improve console streaming
2023-06-04 21:42:13 +02:00
Marcel Baumgartner
233c304b3c Fixed error when closing a failed websocket connection 2023-06-04 21:41:15 +02:00
Marcel Baumgartner
343e527fb6 Added message cache clear for console streaming 2023-06-04 20:56:47 +02:00
Daniel Balk
25da3c233e Merge pull request #138 from Dannyx1604/main
Ein paar kleine Änderungen ;-)
2023-06-03 12:48:43 +02:00
Dannyx
d7fb3382f7 Ein paar kleine Änderungen ;-) 2023-06-03 09:06:22 +02:00
Marcel Baumgartner
88c9f5372d Merge pull request #137 from Moonlight-Panel/AddConsoleStreamingDispose
Add console streaming dispose
2023-06-01 00:44:56 +02:00
Marcel Baumgartner
7128a7f8a7 Add console streaming dispose 2023-06-01 00:44:14 +02:00
Marcel Baumgartner
6e4f1c1dbd Merge pull request #135 from Moonlight-Panel/FixUnarchiveCrash
Fixed decompress issue (hopefully)
2023-05-28 04:33:30 +02:00
Marcel Baumgartner
3527bc1bd5 Fixed decompress issue (hopefully) 2023-05-28 04:33:11 +02:00
Marcel Baumgartner
c0068b58d7 Merge pull request #134 from Moonlight-Panel/AddNewConsoleStreaming
Added new console streaming
2023-05-28 04:27:26 +02:00
Marcel Baumgartner
feec9426b9 Added new console streaming 2023-05-28 04:27:00 +02:00
Marcel Baumgartner
a180cfa31d Merge pull request #133 from Moonlight-Panel/PatchEventSystem
Patched event system
2023-05-28 03:40:43 +02:00
Marcel Baumgartner
b270b48ac1 Patched event system. Storage issues when using the support chat should be fixed 2023-05-28 03:39:36 +02:00
Marcel Baumgartner
a92c34b47f Merge pull request #132 from Moonlight-Panel/AddDiscordLinkSettings
Implemented new discord linking system
2023-05-26 17:14:28 +02:00
Marcel Baumgartner
ac3bdba3e8 Implemented new discord linking system 2023-05-26 17:14:06 +02:00
Marcel Baumgartner
93328b8b88 Merge pull request #131 from Moonlight-Panel/UIFixes
Added new beta server list ui and added days to uptime formatter
2023-05-26 15:21:16 +02:00
Marcel Baumgartner
800f9fbb50 Added new beta server list ui and added days to uptime formatter 2023-05-26 15:18:59 +02:00
91 changed files with 4559 additions and 1216 deletions

1
.gitignore vendored
View File

@@ -42,3 +42,4 @@ Desktop.ini
storage/
Moonlight/publish.ps1
Moonlight/version

View File

@@ -1,5 +1,4 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Newtonsoft.Json;
using RestSharp;
@@ -14,26 +13,13 @@ public class DaemonApiHelper
Client = new();
}
private string GetApiUrl(Node node)
{
/* SSL not implemented in moonlight daemon
if(node.Ssl)
return $"https://{node.Fqdn}:{node.MoonlightDaemonPort}/";
else
return $"http://{node.Fqdn}:{node.MoonlightDaemonPort}/";*/
return $"http://{node.Fqdn}:{node.MoonlightDaemonPort}/";
}
public async Task<T> Get<T>(Node node, string resource)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = await CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", node.Token);
request.Method = Method.Get;
var response = await Client.GetAsync(request);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@@ -52,4 +38,69 @@ public class DaemonApiHelper
return JsonConvert.DeserializeObject<T>(response.Content!)!;
}
public async Task Post(Node node, string resource, object body)
{
var request = await CreateRequest(node, resource);
request.Method = Method.Post;
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
if (response.StatusCode != 0)
{
throw new DaemonException(
$"An error occured: ({response.StatusCode}) {response.Content}",
(int)response.StatusCode
);
}
else
{
throw new Exception($"An internal error occured: {response.ErrorMessage}");
}
}
}
public async Task Delete(Node node, string resource, object body)
{
var request = await CreateRequest(node, resource);
request.Method = Method.Delete;
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
if (response.StatusCode != 0)
{
throw new DaemonException(
$"An error occured: ({response.StatusCode}) {response.Content}",
(int)response.StatusCode
);
}
else
{
throw new Exception($"An internal error occured: {response.ErrorMessage}");
}
}
}
private Task<RestRequest> CreateRequest(Node node, string resource)
{
var url = $"http://{node.Fqdn}:{node.MoonlightDaemonPort}/";
RestRequest request = new(url + resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", node.Token);
return Task.FromResult(request);
}
}

View File

@@ -0,0 +1,8 @@
namespace Moonlight.App.ApiClients.Daemon.Requests;
public class Mount
{
public string Server { get; set; } = "";
public string ServerPath { get; set; } = "";
public string Path { get; set; } = "";
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.App.ApiClients.Daemon.Requests;
public class Unmount
{
public string Path { get; set; } = "";
}

View File

@@ -0,0 +1,10 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class Container
{
public string Name { get; set; } = "";
public long Memory { get; set; }
public double Cpu { get; set; }
public long NetworkIn { get; set; }
public long NetworkOut { get; set; }
}

View File

@@ -1,15 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class ContainerStats
{
public List<Container> Containers { get; set; } = new();
public class Container
{
public string Name { get; set; }
public long Memory { get; set; }
public double Cpu { get; set; }
public long NetworkIn { get; set; }
public long NetworkOut { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class CpuMetrics
{
public string CpuModel { get; set; } = "";
public double CpuUsage { get; set; }
}

View File

@@ -1,8 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class CpuStats
{
public double Usage { get; set; }
public int Cores { get; set; }
public string Model { get; set; } = "";
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class DiskMetrics
{
public long Used { get; set; }
public long Total { get; set; }
}

View File

@@ -1,9 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class DiskStats
{
public long FreeBytes { get; set; }
public string DriveFormat { get; set; }
public string Name { get; set; }
public long TotalSize { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class DockerMetrics
{
public Container[] Containers { get; set; } = Array.Empty<Container>();
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class MemoryMetrics
{
public long Used { get; set; }
public long Total { get; set; }
}

View File

@@ -1,15 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class MemoryStats
{
public List<MemoryStick> Sticks { get; set; } = new();
public double Free { get; set; }
public double Used { get; set; }
public double Total { get; set; }
public class MemoryStick
{
public int Size { get; set; }
public string Type { get; set; } = "";
}
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class SystemMetrics
{
public string OsName { get; set; } = "";
public long Uptime { get; set; }
}

View File

@@ -20,4 +20,5 @@ public class Image
public List<DockerImage> DockerImages { get; set; } = new();
public List<ImageVariable> Variables { get; set; } = new();
public string TagsJson { get; set; } = "";
public string BackgroundImageUrl { get; set; } = "";
}

View File

@@ -43,6 +43,7 @@ public class User
// Date stuff
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime LastVisitedAt { get; set; } = DateTime.UtcNow;
// Subscriptions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddBackgroundImageUrlImage : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "BackgroundImageUrl",
table: "Images",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BackgroundImageUrl",
table: "Images");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddLastVisitedTimestamp : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "LastVisitedAt",
table: "Users",
type: "datetime(6)",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastVisitedAt",
table: "Users");
}
}
}

View File

@@ -132,6 +132,10 @@ namespace Moonlight.App.Database.Migrations
b.Property<int>("Allocations")
.HasColumnType("int");
b.Property<string>("BackgroundImageUrl")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ConfigFiles")
.IsRequired()
.HasColumnType("longtext");
@@ -758,6 +762,9 @@ namespace Moonlight.App.Database.Migrations
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("LastVisitedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("longtext");

View File

@@ -0,0 +1,58 @@
using System.Diagnostics;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Moonlight.App.Database.Entities;
using Moonlight.App.Repositories;
using Moonlight.App.Services;
namespace Moonlight.App.Diagnostics.HealthChecks;
public class DaemonHealthCheck : IHealthCheck
{
private readonly Repository<Node> NodeRepository;
private readonly NodeService NodeService;
public DaemonHealthCheck(Repository<Node> nodeRepository, NodeService nodeService)
{
NodeRepository = nodeRepository;
NodeService = nodeService;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
{
var nodes = NodeRepository.Get().ToArray();
var results = new Dictionary<Node, bool>();
var healthCheckData = new Dictionary<string, object>();
foreach (var node in nodes)
{
try
{
await NodeService.GetCpuMetrics(node);
results.Add(node, true);
}
catch (Exception e)
{
results.Add(node, false);
healthCheckData.Add(node.Name, e.ToStringDemystified());
}
}
var offlineNodes = results
.Where(x => !x.Value)
.ToArray();
if (offlineNodes.Length == nodes.Length)
{
return HealthCheckResult.Unhealthy("All node daemons are offline", null, healthCheckData);
}
if (offlineNodes.Length == 0)
{
return HealthCheckResult.Healthy("All node daemons are online");
}
return HealthCheckResult.Degraded($"{offlineNodes.Length} node daemons are offline", null, healthCheckData);
}
}

View File

@@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Moonlight.App.Database;
namespace Moonlight.App.Diagnostics.HealthChecks;
public class DatabaseHealthCheck : IHealthCheck
{
private readonly DataContext DataContext;
public DatabaseHealthCheck(DataContext dataContext)
{
DataContext = dataContext;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = new CancellationToken())
{
try
{
await DataContext.Database.OpenConnectionAsync(cancellationToken);
await DataContext.Database.CloseConnectionAsync();
return HealthCheckResult.Healthy("Database is online");
}
catch (Exception e)
{
return HealthCheckResult.Unhealthy("Database is offline", e);
}
}
}

View File

@@ -0,0 +1,58 @@
using System.Diagnostics;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Moonlight.App.Database.Entities;
using Moonlight.App.Repositories;
using Moonlight.App.Services;
namespace Moonlight.App.Diagnostics.HealthChecks;
public class NodeHealthCheck : IHealthCheck
{
private readonly Repository<Node> NodeRepository;
private readonly NodeService NodeService;
public NodeHealthCheck(Repository<Node> nodeRepository, NodeService nodeService)
{
NodeRepository = nodeRepository;
NodeService = nodeService;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
{
var nodes = NodeRepository.Get().ToArray();
var results = new Dictionary<Node, bool>();
var healthCheckData = new Dictionary<string, object>();
foreach (var node in nodes)
{
try
{
await NodeService.GetStatus(node);
results.Add(node, true);
}
catch (Exception e)
{
results.Add(node, false);
healthCheckData.Add(node.Name, e.ToStringDemystified());
}
}
var offlineNodes = results
.Where(x => !x.Value)
.ToArray();
if (offlineNodes.Length == nodes.Length)
{
return HealthCheckResult.Unhealthy("All nodes are offline", null, healthCheckData);
}
if (offlineNodes.Length == 0)
{
return HealthCheckResult.Healthy("All nodes are online");
}
return HealthCheckResult.Degraded($"{offlineNodes.Length} nodes are offline", null, healthCheckData);
}
}

View File

@@ -5,7 +5,6 @@ namespace Moonlight.App.Events;
public class EventSystem
{
private Dictionary<int, object> Storage = new();
private List<Subscriber> Subscribers = new();
private readonly bool Debug = false;
@@ -33,16 +32,8 @@ public class EventSystem
return Task.CompletedTask;
}
public Task Emit(string id, object? d = null)
public Task Emit(string id, object? data = null)
{
int hashCode = -1;
if (d != null)
{
hashCode = d.GetHashCode();
Storage.TryAdd(hashCode, d);
}
Subscriber[] subscribers;
lock (Subscribers)
@@ -58,23 +49,6 @@ public class EventSystem
{
tasks.Add(new Task(() =>
{
int storageId = hashCode + 0; // To create a copy of the hash code
object? data = null;
if (storageId != -1)
{
if (Storage.TryGetValue(storageId, out var value))
{
data = value;
}
else
{
Logger.Warn($"Object with the hash '{storageId}' was not present in the storage");
return;
}
}
var stopWatch = new Stopwatch();
stopWatch.Start();
@@ -115,7 +89,6 @@ public class EventSystem
Task.Run(() =>
{
Task.WaitAll(tasks.ToArray());
Storage.Remove(hashCode);
if(Debug)
Logger.Debug($"Completed all event tasks for '{id}' and removed object from storage");

View File

@@ -0,0 +1,39 @@
using Moonlight.App.Database.Entities;
namespace Moonlight.App.Helpers;
public static class AvgHelper
{
public static StatisticsData[] Calculate(StatisticsData[] data, int splitSize = 40)
{
if (data.Length <= splitSize)
return data;
var result = new List<StatisticsData>();
var i = data.Length / (float)splitSize;
var pc = (int)Math.Round(i);
foreach (var part in data.Chunk(pc))
{
double d = 0;
var res = new StatisticsData();
foreach (var entry in part)
{
d += entry.Value;
}
res.Chart = part.First().Chart;
res.Date = part.First().Date;
if (d == 0)
res.Value = 0;
res.Value = d / part.Length;
result.Add(res);
}
return result.ToArray();
}
}

View File

@@ -3,6 +3,7 @@ using Moonlight.App.ApiClients.Wings;
using Moonlight.App.ApiClients.Wings.Requests;
using Moonlight.App.ApiClients.Wings.Resources;
using Moonlight.App.Database.Entities;
using Moonlight.App.Helpers.Wings;
using Moonlight.App.Services;
using RestSharp;
@@ -210,6 +211,8 @@ public class WingsFileAccess : FileAccess
}
public override async Task Decompress(FileData fileData)
{
try
{
var req = new DecompressFile()
{
@@ -219,6 +222,18 @@ public class WingsFileAccess : FileAccess
await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/decompress", req);
}
catch (Exception e)
{
if (e.Message.ToLower().Contains("canceled"))
{
// ignore, maybe do smth better here, like showing a waiting thing or so
}
else
{
throw;
}
}
}
public override Task<string> GetLaunchUrl()
{

View File

@@ -8,8 +8,27 @@ public static class Formatter
{
TimeSpan t = TimeSpan.FromMilliseconds(uptime);
if (t.Days > 0)
{
return $"{t.Days}d {t.Hours}h {t.Minutes}m {t.Seconds}s";
}
else
{
return $"{t.Hours}h {t.Minutes}m {t.Seconds}s";
}
}
public static string FormatUptime(TimeSpan t)
{
if (t.Days > 0)
{
return $"{t.Days}d {t.Hours}h {t.Minutes}m {t.Seconds}s";
}
else
{
return $"{t.Hours}h {t.Minutes}m {t.Seconds}s";
}
}
private static double Round(this double d, int decimals)
{
@@ -109,4 +128,12 @@ public static class Formatter
return (i / (1024D * 1024D)).Round(2) + " GB";
}
}
public static double BytesToGb(long bytes)
{
const double gbMultiplier = 1024 * 1024 * 1024; // 1 GB = 1024 MB * 1024 KB * 1024 B
double gigabytes = (double)bytes / gbMultiplier;
return gigabytes;
}
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Helpers.Wings.Data;
public class ConsoleMessage
{
public string Content { get; set; } = "";
public bool IsInternal { get; set; } = false;
}

View File

@@ -0,0 +1,36 @@
using Newtonsoft.Json;
namespace Moonlight.App.Helpers.Wings.Data;
public class ServerResource
{
[JsonProperty("memory_bytes")]
public long MemoryBytes { get; set; }
[JsonProperty("memory_limit_bytes")]
public long MemoryLimitBytes { get; set; }
[JsonProperty("cpu_absolute")]
public float CpuAbsolute { get; set; }
[JsonProperty("network")]
public NetworkData Network { get; set; }
[JsonProperty("uptime")]
public double Uptime { get; set; }
[JsonProperty("state")]
public string State { get; set; }
[JsonProperty("disk_bytes")]
public long DiskBytes { get; set; }
public class NetworkData
{
[JsonProperty("rx_bytes")]
public long RxBytes { get; set; }
[JsonProperty("tx_bytes")]
public long TxBytes { get; set; }
}
}

View File

@@ -0,0 +1,8 @@
namespace Moonlight.App.Helpers.Wings.Enums;
public enum ConsoleState
{
Disconnected,
Connecting,
Connected
}

View File

@@ -0,0 +1,10 @@
namespace Moonlight.App.Helpers.Wings.Enums;
public enum ServerState
{
Starting,
Running,
Stopping,
Offline,
Installing
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Helpers.Wings.Events;
public class BaseEvent
{
public string Event { get; set; } = "";
public string[] Args { get; set; } = Array.Empty<string>();
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Helpers.Wings.Events;
public class SendTokenEvent
{
public string Event { get; set; } = "auth";
public List<string> Args = new();
}

View File

@@ -0,0 +1,389 @@
using System.Net.WebSockets;
using System.Text;
using Logging.Net;
using Moonlight.App.Helpers.Wings.Data;
using Moonlight.App.Helpers.Wings.Enums;
using Moonlight.App.Helpers.Wings.Events;
using Newtonsoft.Json;
using ConsoleMessage = Moonlight.App.Helpers.Wings.Data.ConsoleMessage;
namespace Moonlight.App.Helpers.Wings;
public class WingsConsole : IDisposable
{
private ClientWebSocket WebSocket;
public List<ConsoleMessage> Messages;
private Task? ConsoleTask;
private string Socket = "";
private string Origin = "";
private string Token = "";
private bool Disconnecting;
public ConsoleState ConsoleState { get; private set; }
public ServerState ServerState { get; private set; }
public ServerResource Resource { get; private set; }
public EventHandler<ConsoleState> OnConsoleStateUpdated { get; set; }
public EventHandler<ServerState> OnServerStateUpdated { get; set; }
public EventHandler<ServerResource> OnResourceUpdated { get; set; }
public EventHandler<ConsoleMessage> OnMessage { get; set; }
public Func<WingsConsole, Task<string>> OnRequestNewToken { get; set; }
public WingsConsole()
{
ConsoleState = ConsoleState.Disconnected;
ServerState = ServerState.Offline;
Messages = new();
Resource = new()
{
Network = new()
{
RxBytes = 0,
TxBytes = 0
},
State = "offline",
Uptime = 0,
CpuAbsolute = 0,
DiskBytes = 0,
MemoryBytes = 0,
MemoryLimitBytes = 0
};
}
public Task Connect(string origin, string socket, string token)
{
Disconnecting = false;
WebSocket = new();
ConsoleState = ConsoleState.Disconnected;
ServerState = ServerState.Offline;
Messages = new();
Resource = new()
{
Network = new()
{
RxBytes = 0,
TxBytes = 0
},
State = "offline",
Uptime = 0,
CpuAbsolute = 0,
DiskBytes = 0,
MemoryBytes = 0,
MemoryLimitBytes = 0
};
Socket = socket;
Origin = origin;
Token = token;
WebSocket.Options.SetRequestHeader("Origin", Origin);
WebSocket.Options.SetRequestHeader("Authorization", "Bearer " + Token);
ConsoleTask = Task.Run(async () =>
{
try
{
await Work();
}
catch (Exception e)
{
Logger.Warn("Error connecting to wings console");
Logger.Warn(e);
}
});
return Task.CompletedTask;
}
private async Task Work()
{
await UpdateConsoleState(ConsoleState.Connecting);
await WebSocket.ConnectAsync(
new Uri(Socket),
CancellationToken.None
);
if (WebSocket.State != WebSocketState.Connecting && WebSocket.State != WebSocketState.Open)
{
await SaveMessage("Unable to connect to websocket", true);
await UpdateConsoleState(ConsoleState.Disconnected);
return;
}
await UpdateConsoleState(ConsoleState.Connected);
await Send(new SendTokenEvent()
{
Args = { Token }
});
while (WebSocket.State == WebSocketState.Open)
{
try
{
var raw = await ReceiveRaw();
if(string.IsNullOrEmpty(raw))
continue;
var eventData = JsonConvert.DeserializeObject<BaseEvent>(raw);
if (eventData == null)
{
await SaveMessage("Unable to parse event", true);
continue;
}
switch (eventData.Event)
{
case "jwt error":
if (WebSocket != null)
{
if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open)
await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None);
WebSocket.Dispose();
}
await UpdateServerState(ServerState.Offline);
await UpdateConsoleState(ConsoleState.Disconnected);
await SaveMessage("Received a jwt error. Disconnected", true);
break;
case "token expired":
if (WebSocket != null)
{
if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open)
await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None);
WebSocket.Dispose();
}
await UpdateServerState(ServerState.Offline);
await UpdateConsoleState(ConsoleState.Disconnected);
await SaveMessage("Token expired", true);
break;
case "token expiring":
await SaveMessage("Token will expire soon. Generating a new one", true);
Token = await OnRequestNewToken.Invoke(this);
await Send(new SendTokenEvent()
{
Args = { Token }
});
break;
case "auth success":
// Send intents
await SendRaw("{\"event\":\"send logs\",\"args\":[null]}");
await SendRaw("{\"event\":\"send stats\",\"args\":[null]}");
break;
case "stats":
var stats = JsonConvert.DeserializeObject<ServerResource>(eventData.Args[0]);
if (stats == null)
break;
var serverState = ParseServerState(stats.State);
if (ServerState != serverState)
await UpdateServerState(serverState);
await UpdateResource(stats);
break;
case "status":
var serverStateParsed = ParseServerState(eventData.Args[0]);
if (ServerState != serverStateParsed)
await UpdateServerState(serverStateParsed);
break;
case "console output":
foreach (var line in eventData.Args)
{
await SaveMessage(line);
}
break;
case "install output":
foreach (var line in eventData.Args)
{
await SaveMessage(line);
}
break;
case "daemon message":
foreach (var line in eventData.Args)
{
await SaveMessage(line);
}
break;
case "install started":
await UpdateServerState(ServerState.Installing);
break;
case "install completed":
await UpdateServerState(ServerState.Offline);
break;
}
}
catch (Exception e)
{
if (!Disconnecting)
{
Logger.Warn("Error while performing websocket actions");
Logger.Warn(e);
await SaveMessage("A unknown error occured while processing websocket", true);
}
}
}
}
private Task UpdateConsoleState(ConsoleState consoleState)
{
ConsoleState = consoleState;
OnConsoleStateUpdated?.Invoke(this, consoleState);
return Task.CompletedTask;
}
private Task UpdateServerState(ServerState serverState)
{
ServerState = serverState;
OnServerStateUpdated?.Invoke(this, serverState);
return Task.CompletedTask;
}
private Task UpdateResource(ServerResource resource)
{
Resource = resource;
OnResourceUpdated?.Invoke(this, Resource);
return Task.CompletedTask;
}
private Task SaveMessage(string content, bool internalMessage = false)
{
var msg = new ConsoleMessage()
{
Content = content,
IsInternal = internalMessage
};
lock (Messages)
{
Messages.Add(msg);
}
OnMessage?.Invoke(this, msg);
return Task.CompletedTask;
}
private ServerState ParseServerState(string raw)
{
switch (raw)
{
case "offline":
return ServerState.Offline;
case "starting":
return ServerState.Starting;
case "running":
return ServerState.Running;
case "stopping":
return ServerState.Stopping;
case "installing":
return ServerState.Installing;
default:
return ServerState.Offline;
}
}
public async Task EnterCommand(string content)
{
if (ConsoleState == ConsoleState.Connected)
{
await SendRaw("{\"event\":\"send command\",\"args\":[\"" + content + "\"]}");
}
}
public async Task SetPowerState(string state)
{
if (ConsoleState == ConsoleState.Connected)
{
await SendRaw("{\"event\":\"set state\",\"args\":[\"" + state + "\"]}");
}
}
private async Task Send(object data)
{
await SendRaw(JsonConvert.SerializeObject(data));
}
private async Task SendRaw(string data)
{
if (WebSocket.State == WebSocketState.Open)
{
byte[] byteContentBuffer = Encoding.UTF8.GetBytes(data);
await WebSocket.SendAsync(new ArraySegment<byte>(byteContentBuffer), WebSocketMessageType.Text, true,
CancellationToken.None);
}
}
private async Task<string> ReceiveRaw()
{
ArraySegment<byte> receivedBytes = new ArraySegment<byte>(new byte[1024]);
WebSocketReceiveResult result = await WebSocket.ReceiveAsync(receivedBytes, CancellationToken.None);
return Encoding.UTF8.GetString(receivedBytes.Array!, 0, result.Count);
}
public async Task Disconnect()
{
Disconnecting = true;
Messages.Clear();
if (WebSocket != null)
{
if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open)
await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None);
WebSocket.Dispose();
}
if(ConsoleTask != null && ConsoleTask.IsCompleted)
ConsoleTask.Dispose();
}
public void Dispose()
{
Disconnecting = true;
Messages.Clear();
if (WebSocket != null)
{
if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open)
WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None).Wait();
WebSocket.Dispose();
}
if(ConsoleTask != null && ConsoleTask.IsCompleted)
ConsoleTask.Dispose();
}
}

View File

@@ -7,37 +7,34 @@ using Moonlight.App.Database.Entities;
using Moonlight.App.Repositories.Servers;
using Moonlight.App.Services;
namespace Moonlight.App.Helpers;
namespace Moonlight.App.Helpers.Wings;
public class WingsConsoleHelper
{
private readonly ServerRepository ServerRepository;
private readonly WingsJwtHelper WingsJwtHelper;
private readonly string AppUrl;
public WingsConsoleHelper(
ServerRepository serverRepository,
ConfigService configService,
WingsJwtHelper wingsJwtHelper)
ConfigService configService)
{
ServerRepository = serverRepository;
WingsJwtHelper = wingsJwtHelper;
AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl");
}
public async Task ConnectWings(PteroConsole.NET.PteroConsole pteroConsole, Server server)
public async Task ConnectWings(WingsConsole console, Server server)
{
var serverData = ServerRepository
.Get()
.Include(x => x.Node)
.First(x => x.Id == server.Id);
var token = GenerateToken(serverData);
var token = await GenerateToken(serverData);
if (serverData.Node.Ssl)
{
await pteroConsole.Connect(
await console.Connect(
AppUrl,
$"wss://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws",
token
@@ -45,7 +42,7 @@ public class WingsConsoleHelper
}
else
{
await pteroConsole.Connect(
await console.Connect(
AppUrl,
$"ws://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws",
token
@@ -53,7 +50,7 @@ public class WingsConsoleHelper
}
}
public string GenerateToken(Server server)
public async Task<string> GenerateToken(Server server)
{
var serverData = ServerRepository
.Get()
@@ -66,7 +63,7 @@ public class WingsConsoleHelper
using (MD5 md5 = MD5.Create())
{
var inputBytes = Encoding.ASCII.GetBytes(userid + serverData.Uuid.ToString());
var inputBytes = Encoding.ASCII.GetBytes(userid + server.Uuid.ToString());
var outputBytes = md5.ComputeHash(inputBytes);
var identifier = Convert.ToHexString(outputBytes).ToLower();
@@ -77,7 +74,7 @@ public class WingsConsoleHelper
.WithAlgorithm(new HMACSHA256Algorithm())
.WithSecret(secret)
.AddClaim("user_id", userid)
.AddClaim("server_uuid", serverData.Uuid.ToString())
.AddClaim("server_uuid", server.Uuid.ToString())
.AddClaim("permissions", new[]
{
"*",

View File

@@ -4,7 +4,7 @@ using JWT.Algorithms;
using JWT.Builder;
using Moonlight.App.Services;
namespace Moonlight.App.Helpers;
namespace Moonlight.App.Helpers.Wings;
public class WingsJwtHelper
{

View File

@@ -5,7 +5,7 @@ using Moonlight.App.Http.Resources.Wings;
using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers;
namespace Moonlight.App.Helpers;
namespace Moonlight.App.Helpers.Wings;
public class WingsServerConverter
{

View File

@@ -1,6 +1,7 @@
using Logging.Net;
using Microsoft.AspNetCore.Mvc;
using Moonlight.App.Services;
using Moonlight.App.Services.Sessions;
namespace Moonlight.App.Http.Controllers.Api.Moonlight;
@@ -11,12 +12,41 @@ public class OAuth2Controller : Controller
private readonly UserService UserService;
private readonly OAuth2Service OAuth2Service;
private readonly DateTimeService DateTimeService;
private readonly IdentityService IdentityService;
public OAuth2Controller(UserService userService, OAuth2Service oAuth2Service, DateTimeService dateTimeService)
public OAuth2Controller(
UserService userService,
OAuth2Service oAuth2Service,
DateTimeService dateTimeService,
IdentityService identityService)
{
UserService = userService;
OAuth2Service = oAuth2Service;
DateTimeService = dateTimeService;
IdentityService = identityService;
}
[HttpGet("{id}/start")]
public async Task<ActionResult> Start([FromRoute] string id)
{
try
{
if (OAuth2Service.Providers.ContainsKey(id))
{
return Redirect(await OAuth2Service.GetUrl(id));
}
Logger.Warn($"Someone tried to start an oauth2 flow using the id '{id}' which is not registered");
return Redirect("/");
}
catch (Exception e)
{
Logger.Warn($"Error starting oauth2 flow for id: {id}");
Logger.Warn(e);
return Redirect("/");
}
}
[HttpGet("{id}")]
@@ -24,6 +54,18 @@ public class OAuth2Controller : Controller
{
try
{
var currentUser = await IdentityService.Get();
if (currentUser != null)
{
if (await OAuth2Service.CanBeLinked(id))
{
await OAuth2Service.LinkToUser(id, currentUser, code);
return Redirect("/profile");
}
}
var user = await OAuth2Service.HandleCode(id, code);
Response.Cookies.Append("token", await UserService.GenerateToken(user), new()

View File

@@ -16,14 +16,12 @@ public class ResourcesController : Controller
{
private readonly SecurityLogService SecurityLogService;
private readonly BucketService BucketService;
private readonly BundleService BundleService;
public ResourcesController(SecurityLogService securityLogService,
BucketService bucketService, BundleService bundleService)
BucketService bucketService)
{
SecurityLogService = securityLogService;
BucketService = bucketService;
BundleService = bundleService;
}
[HttpGet("images/{name}")]
@@ -49,6 +47,29 @@ public class ResourcesController : Controller
return NotFound();
}
[HttpGet("background/{name}")]
public async Task<ActionResult> GetBackground([FromRoute] string name)
{
if (name.Contains(".."))
{
await SecurityLogService.Log(SecurityLogType.PathTransversal, x =>
{
x.Add<string>(name);
});
return NotFound();
}
if (System.IO.File.Exists(PathBuilder.File("storage", "resources", "public", "background", name)))
{
var fs = new FileStream(PathBuilder.File("storage", "resources", "public", "background", name), FileMode.Open);
return File(fs, MimeTypes.GetMimeType(name), name);
}
return NotFound();
}
[HttpGet("bucket/{bucket}/{name}")]
public async Task<ActionResult> GetBucket([FromRoute] string bucket, [FromRoute] string name)
{
@@ -77,34 +98,4 @@ public class ResourcesController : Controller
return Problem();
}
}
[HttpGet("bundle/js")]
public Task<ActionResult> GetJs()
{
if (BundleService.BundledFinished)
{
return Task.FromResult<ActionResult>(
File(Encoding.ASCII.GetBytes(BundleService.BundledJs), "text/javascript")
);
}
return Task.FromResult<ActionResult>(
NotFound()
);
}
[HttpGet("bundle/css")]
public Task<ActionResult> GetCss()
{
if (BundleService.BundledFinished)
{
return Task.FromResult<ActionResult>(
File(Encoding.ASCII.GetBytes(BundleService.BundledCss), "text/css")
);
}
return Task.FromResult<ActionResult>(
NotFound()
);
}
}

View File

@@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Events;
using Moonlight.App.Helpers;
using Moonlight.App.Helpers.Wings;
using Moonlight.App.Http.Resources.Wings;
using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers;

View File

@@ -0,0 +1,19 @@
using Newtonsoft.Json;
namespace Moonlight.App.Models.Misc;
public class HealthCheck
{
public string Status { get; set; }
public TimeSpan TotalDuration { get; set; }
public Dictionary<string, HealthCheckEntry> Entries { get; set; } = new();
public class HealthCheckEntry
{
public Dictionary<string, string> Data { get; set; } = new();
public string Description { get; set; }
public TimeSpan Duration { get; set; }
public string Status { get; set; }
public List<string> Tags { get; set; } = new();
}
}

View File

@@ -6,5 +6,5 @@ namespace Moonlight.App.Models.Misc;
public class RunningServer
{
public Server Server { get; set; }
public ContainerStats.Container Container { get; set; }
public Container Container { get; set; }
}

View File

@@ -9,7 +9,9 @@ public abstract class OAuth2Provider
public string Url { get; set; }
public IServiceScopeFactory ServiceScopeFactory { get; set; }
public string DisplayName { get; set; }
public bool CanBeLinked { get; set; } = false;
public abstract Task<string> GetUrl();
public abstract Task<User> HandleCode(string code);
public abstract Task LinkToUser(User user, string code);
}

View File

@@ -12,6 +12,11 @@ namespace Moonlight.App.OAuth2.Providers;
public class DiscordOAuth2Provider : OAuth2Provider
{
public DiscordOAuth2Provider()
{
CanBeLinked = true;
}
public override Task<string> GetUrl()
{
string url = $"https://discord.com/api/oauth2/authorize?client_id={Config.ClientId}" +
@@ -119,4 +124,74 @@ public class DiscordOAuth2Provider : OAuth2Provider
return user;
}
}
public override async Task LinkToUser(User user, string code)
{
// Endpoints
var endpoint = Url + "/api/moonlight/oauth2/discord";
var discordUserDataEndpoint = "https://discordapp.com/api/users/@me";
var discordEndpoint = "https://discordapp.com/api/oauth2/token";
// Generate access token
using var client = new RestClient();
var request = new RestRequest(discordEndpoint);
request.AddParameter("client_id", Config.ClientId);
request.AddParameter("client_secret", Config.ClientSecret);
request.AddParameter("grant_type", "authorization_code");
request.AddParameter("code", code);
request.AddParameter("redirect_uri", endpoint);
var response = await client.ExecutePostAsync(request);
if (!response.IsSuccessful)
{
Logger.Warn("Error verifying oauth2 code");
Logger.Warn(response.ErrorMessage);
throw new DisplayException("An error occured while verifying oauth2 code");
}
// parse response
var data = new ConfigurationBuilder().AddJsonStream(
new MemoryStream(Encoding.ASCII.GetBytes(response.Content!))
).Build();
var accessToken = data.GetValue<string>("access_token");
// Now, we will call the discord api with our access token to get the data we need
var getRequest = new RestRequest(discordUserDataEndpoint);
getRequest.AddHeader("Authorization", $"Bearer {accessToken}");
var getResponse = await client.ExecuteGetAsync(getRequest);
if (!getResponse.IsSuccessful)
{
Logger.Warn("An unexpected error occured while fetching user data from remote api");
Logger.Warn(getResponse.ErrorMessage);
throw new DisplayException("An unexpected error occured while fetching user data from remote api");
}
// Parse response
var getData = new ConfigurationBuilder().AddJsonStream(
new MemoryStream(Encoding.ASCII.GetBytes(getResponse.Content!))
).Build();
var id = getData.GetValue<ulong>("id");
// Handle data
using var scope = ServiceScopeFactory.CreateScope();
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
user.DiscordId = id;
userRepo.Update(user);
}
}

View File

@@ -4,7 +4,6 @@ using Moonlight.App.ApiClients.Google.Requests;
using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories;
using Moonlight.App.Services;
using RestSharp;
@@ -13,6 +12,11 @@ namespace Moonlight.App.OAuth2.Providers;
public class GoogleOAuth2Provider : OAuth2Provider
{
public GoogleOAuth2Provider()
{
CanBeLinked = false;
}
public override Task<string> GetUrl()
{
var endpoint = Url + "/api/moonlight/oauth2/google";
@@ -127,4 +131,9 @@ public class GoogleOAuth2Provider : OAuth2Provider
return user;
}
}
public override Task LinkToUser(User user, string code)
{
throw new NotImplementedException();
}
}

View File

@@ -5,6 +5,7 @@ using Moonlight.App.ApiClients.Daemon.Resources;
using Moonlight.App.ApiClients.Wings;
using Moonlight.App.Database.Entities;
using Moonlight.App.Events;
using Moonlight.App.Helpers;
using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers;
using Newtonsoft.Json;
@@ -81,12 +82,12 @@ public class CleanupService
{
try
{
var cpuStats = await nodeService.GetCpuStats(node);
var memoryStats = await nodeService.GetMemoryStats(node);
var cpuMetrics = await nodeService.GetCpuMetrics(node);
var memoryMetrics = await nodeService.GetMemoryMetrics(node);
if (cpuStats.Usage > maxCpu || memoryStats.Free < minMemory)
if (cpuMetrics.CpuUsage > maxCpu || (Formatter.BytesToGb(memoryMetrics.Total) - (Formatter.BytesToGb(memoryMetrics.Used))) < minMemory)
{
var containerStats = await nodeService.GetContainerStats(node);
var dockerMetrics = await nodeService.GetDockerMetrics(node);
var serverRepository = scope.ServiceProvider.GetRequiredService<ServerRepository>();
var imageRepository = scope.ServiceProvider.GetRequiredService<ImageRepository>();
@@ -101,9 +102,9 @@ public class CleanupService
)
.ToArray();
var containerMappedToServers = new Dictionary<ContainerStats.Container, Server>();
var containerMappedToServers = new Dictionary<Container, Server>();
foreach (var container in containerStats.Containers)
foreach (var container in dockerMetrics.Containers)
{
if (Guid.TryParse(container.Name, out Guid uuid))
{

View File

@@ -16,6 +16,11 @@ public class ResourceService
return $"{AppUrl}/api/moonlight/resources/images/{name}";
}
public string BackgroundImage(string name)
{
return $"{AppUrl}/api/moonlight/resources/background/{name}";
}
public string Avatar(User user)
{
return $"{AppUrl}/api/moonlight/avatar/{user.Id}";

View File

@@ -0,0 +1,101 @@
using Logging.Net;
using Octokit;
using Repository = LibGit2Sharp.Repository;
namespace Moonlight.App.Services;
public class MoonlightService
{
private readonly ConfigService ConfigService;
public readonly DateTime StartTimestamp;
public readonly string AppVersion;
public readonly List<string[]> ChangeLog = new();
public MoonlightService(ConfigService configService)
{
ConfigService = configService;
StartTimestamp = DateTime.UtcNow;
if (File.Exists("version") && !ConfigService.DebugMode)
AppVersion = File.ReadAllText("version");
else if (ConfigService.DebugMode)
{
string repositoryPath = Path.GetFullPath("..");
using var repo = new Repository(repositoryPath);
var commit = repo.Head.Tip;
AppVersion = commit.Sha;
}
else
AppVersion = "unknown";
Task.Run(FetchChangeLog);
}
private async Task FetchChangeLog()
{
if(AppVersion == "unknown")
return;
if (ConfigService.DebugMode)
{
ChangeLog.Add(new[]
{
"Disabled",
"Fetching changelog from github is disabled in debug mode"
});
return;
}
try
{
var client = new GitHubClient(new ProductHeaderValue("Moonlight"));
var pullRequests = await client.PullRequest.GetAllForRepository("Moonlight-Panel", "Moonlight", new PullRequestRequest
{
State = ItemStateFilter.Closed,
SortDirection = SortDirection.Ascending,
SortProperty = PullRequestSort.Created
});
var groupedPullRequests = new Dictionary<DateTime, List<string>>();
foreach (var pullRequest in pullRequests)
{
if (pullRequest.MergedAt != null)
{
var date = pullRequest.MergedAt.Value.Date;
if (!groupedPullRequests.ContainsKey(date))
{
groupedPullRequests[date] = new List<string>();
}
groupedPullRequests[date].Add(pullRequest.Title);
}
}
int i = 1;
foreach (var group in groupedPullRequests)
{
var pullRequestsList = new List<string>();
var date = group.Key.ToString("dd.MM.yyyy");
pullRequestsList.Add($"Patch {i}, {date}");
foreach (var pullRequest in group.Value)
{
pullRequestsList.Add(pullRequest);
}
ChangeLog.Add(pullRequestsList.ToArray());
i++;
}
}
catch (Exception e)
{
Logger.Warn("Error fetching changelog");
Logger.Warn(e);
}
}
}

View File

@@ -1,4 +1,5 @@
using Moonlight.App.ApiClients.Daemon;
using Moonlight.App.ApiClients.Daemon.Requests;
using Moonlight.App.ApiClients.Daemon.Resources;
using Moonlight.App.ApiClients.Wings;
using Moonlight.App.ApiClients.Wings.Resources;
@@ -24,34 +25,55 @@ public class NodeService
return await WingsApiHelper.Get<SystemStatus>(node, "api/system");
}
public async Task<CpuStats> GetCpuStats(Node node)
public async Task<CpuMetrics> GetCpuMetrics(Node node)
{
return await DaemonApiHelper.Get<CpuStats>(node, "stats/cpu");
return await DaemonApiHelper.Get<CpuMetrics>(node, "metrics/cpu");
}
public async Task<MemoryStats> GetMemoryStats(Node node)
public async Task<MemoryMetrics> GetMemoryMetrics(Node node)
{
return await DaemonApiHelper.Get<MemoryStats>(node, "stats/memory");
return await DaemonApiHelper.Get<MemoryMetrics>(node, "metrics/memory");
}
public async Task<DiskStats> GetDiskStats(Node node)
public async Task<DiskMetrics> GetDiskMetrics(Node node)
{
return await DaemonApiHelper.Get<DiskStats>(node, "stats/disk");
return await DaemonApiHelper.Get<DiskMetrics>(node, "metrics/disk");
}
public async Task<ContainerStats> GetContainerStats(Node node)
public async Task<SystemMetrics> GetSystemMetrics(Node node)
{
return await DaemonApiHelper.Get<ContainerStats>(node, "stats/container");
return await DaemonApiHelper.Get<SystemMetrics>(node, "metrics/system");
}
public async Task<DockerMetrics> GetDockerMetrics(Node node)
{
return await DaemonApiHelper.Get<DockerMetrics>(node, "metrics/docker");
}
public async Task Mount(Node node, string server, string serverPath, string path)
{
await DaemonApiHelper.Post(node, "mount", new Mount()
{
Server = server,
ServerPath = serverPath,
Path = path
});
}
public async Task Unmount(Node node, string path)
{
await DaemonApiHelper.Delete(node, "mount", new Unmount()
{
Path = path
});
}
public async Task<bool> IsHostUp(Node node)
{
try
{
//TODO: Implement status caching
var data = await GetStatus(node);
await GetSystemMetrics(node);
if (data != null)
return true;
}
catch (Exception)

View File

@@ -80,6 +80,26 @@ public class OAuth2Service
return await provider.HandleCode(code);
}
public Task<bool> CanBeLinked(string id)
{
if (Providers.All(x => x.Key != id))
throw new DisplayException("Invalid oauth2 id");
var provider = Providers[id];
return Task.FromResult(provider.CanBeLinked);
}
public async Task LinkToUser(string id, User user, string code)
{
if (Providers.All(x => x.Key != id))
throw new DisplayException("Invalid oauth2 id");
var provider = Providers[id];
await provider.LinkToUser(user, code);
}
private string GetAppUrl()
{
if (EnableOverrideUrl)

View File

@@ -8,6 +8,7 @@ using Moonlight.App.Events;
using Moonlight.App.Exceptions;
using Moonlight.App.Helpers;
using Moonlight.App.Helpers.Files;
using Moonlight.App.Helpers.Wings;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers;
@@ -18,6 +19,7 @@ namespace Moonlight.App.Services;
public class ServerService
{
private readonly Repository<ServerVariable> ServerVariablesRepository;
private readonly ServerRepository ServerRepository;
private readonly UserRepository UserRepository;
private readonly ImageRepository ImageRepository;
@@ -49,7 +51,8 @@ public class ServerService
NodeService nodeService,
NodeAllocationRepository nodeAllocationRepository,
DateTimeService dateTimeService,
EventSystem eventSystem)
EventSystem eventSystem,
Repository<ServerVariable> serverVariablesRepository)
{
ServerRepository = serverRepository;
WingsApiHelper = wingsApiHelper;
@@ -66,6 +69,7 @@ public class ServerService
NodeAllocationRepository = nodeAllocationRepository;
DateTimeService = dateTimeService;
Event = eventSystem;
ServerVariablesRepository = serverVariablesRepository;
}
private Server EnsureNodeData(Server s)
@@ -400,17 +404,13 @@ public class ServerService
public async Task Delete(Server s)
{
throw new DisplayException("Deleting a server is currently a bit buggy. So its disabled for your safety");
var server = EnsureNodeData(s);
var backups = await GetBackups(server);
var backups = await GetBackups(s);
foreach (var backup in backups)
{
try
{
await DeleteBackup(server, backup);
await DeleteBackup(s, backup);
}
catch (Exception)
{
@@ -418,9 +418,18 @@ public class ServerService
}
}
var server = ServerRepository
.Get()
.Include(x => x.Variables)
.Include(x => x.Node)
.First(x => x.Id == s.Id);
await WingsApiHelper.Delete(server.Node, $"api/servers/{server.Uuid}", null);
//TODO: Fix empty data models
foreach (var variable in server.Variables.ToArray())
{
ServerVariablesRepository.Delete(variable);
}
server.Allocations = new();
server.MainAllocation = null;
@@ -428,7 +437,6 @@ public class ServerService
server.Backups = new();
ServerRepository.Update(server);
ServerRepository.Delete(server);
}

View File

@@ -1,129 +0,0 @@
using Logging.Net;
namespace Moonlight.App.Services.Sessions;
public class BundleService
{
public BundleService(ConfigService configService)
{
var url = configService
.GetSection("Moonlight")
.GetValue<string>("AppUrl");
#region JS
JsFiles = new();
JsFiles.AddRange(new[]
{
url + "/_framework/blazor.server.js",
url + "/assets/plugins/global/plugins.bundle.js",
url + "/_content/XtermBlazor/XtermBlazor.min.js",
url + "/_content/BlazorTable/BlazorTable.min.js",
url + "/_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js",
url + "/_content/Blazor.ContextMenu/blazorContextMenu.min.js",
"https://www.google.com/recaptcha/api.js",
"https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.min.js",
"https://cdn.jsdelivr.net/npm/xterm-addon-search@0.8.2/lib/xterm-addon-search.min.js",
"https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.5.0/lib/xterm-addon-web-links.min.js",
url + "/_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js",
"require.config({ paths: { 'vs': '/_content/BlazorMonaco/lib/monaco-editor/min/vs' } });",
url + "/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js",
url + "/_content/BlazorMonaco/jsInterop.js",
url + "/assets/js/scripts.bundle.js",
url + "/assets/js/moonlight.js",
"moonlight.loading.registerXterm();",
url + "/_content/Blazor-ApexCharts/js/apex-charts.min.js",
url + "/_content/Blazor-ApexCharts/js/blazor-apex-charts.js"
});
#endregion
#region CSS
CssFiles = new();
CssFiles.AddRange(new[]
{
"https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700",
url + "/assets/css/style.bundle.css",
url + "/assets/css/flashbang.css",
url + "/assets/css/snow.css",
url + "/assets/css/utils.css",
url + "/assets/css/blazor.css",
url + "/_content/XtermBlazor/XtermBlazor.css",
url + "/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.css",
url + "/_content/Blazor.ContextMenu/blazorContextMenu.min.css",
url + "/assets/plugins/global/plugins.bundle.css"
});
#endregion
CacheId = Guid.NewGuid().ToString();
Task.Run(Bundle);
}
// Javascript
public string BundledJs { get; private set; }
public readonly List<string> JsFiles;
// CSS
public string BundledCss { get; private set; }
public readonly List<string> CssFiles;
// States
public string CacheId { get; private set; }
public bool BundledFinished { get; set; } = false;
private bool IsBundling { get; set; } = false;
private async Task Bundle()
{
if (!IsBundling)
IsBundling = true;
Logger.Info("Bundling js and css files");
BundledJs = "";
BundledCss = "";
BundledJs = await BundleFiles(
JsFiles
);
BundledCss = await BundleFiles(
CssFiles
);
Logger.Info("Successfully bundled");
BundledFinished = true;
}
private async Task<string> BundleFiles(IEnumerable<string> items)
{
var bundled = "";
using HttpClient client = new HttpClient();
foreach (string item in items)
{
// Item is a url, fetch it
if (item.StartsWith("http"))
{
try
{
var jsCode = await client.GetStringAsync(item);
bundled += jsCode + "\n";
}
catch (Exception e)
{
Logger.Warn($"Error fetching '{item}' while bundling");
Logger.Warn(e);
}
}
else // If not, it is probably a manual addition, so add it
bundled += item + "\n";
}
return bundled;
}
}

View File

@@ -0,0 +1,39 @@
using Logging.Net;
using Moonlight.App.Services.Files;
namespace Moonlight.App.Services.Sessions;
public class DynamicBackgroundService
{
public EventHandler OnBackgroundImageChanged { get; set; }
public string BackgroundImageUrl { get; private set; }
private string DefaultBackgroundImageUrl;
public DynamicBackgroundService(ResourceService resourceService)
{
DefaultBackgroundImageUrl = resourceService.BackgroundImage("main.jpg");
BackgroundImageUrl = DefaultBackgroundImageUrl;
}
public Task Change(string url)
{
if(BackgroundImageUrl == url) // Prevent unnecessary updates
return Task.CompletedTask;
BackgroundImageUrl = url;
OnBackgroundImageChanged?.Invoke(this, null!);
return Task.CompletedTask;
}
public Task Reset()
{
if(BackgroundImageUrl == DefaultBackgroundImageUrl) // Prevent unnecessary updates
return Task.CompletedTask;
BackgroundImageUrl = DefaultBackgroundImageUrl;
OnBackgroundImageChanged?.Invoke(this, null!);
return Task.CompletedTask;
}
}

View File

@@ -9,6 +9,7 @@ namespace Moonlight.App.Services.Sessions;
public class SessionService
{
private readonly SessionRepository SessionRepository;
private Repository<User> UserRepository;
private readonly IdentityService IdentityService;
private readonly NavigationManager NavigationManager;
private readonly AlertService AlertService;
@@ -21,13 +22,15 @@ public class SessionService
IdentityService identityService,
NavigationManager navigationManager,
AlertService alertService,
DateTimeService dateTimeService)
DateTimeService dateTimeService,
Repository<User> userRepository)
{
SessionRepository = sessionRepository;
IdentityService = identityService;
NavigationManager = navigationManager;
AlertService = alertService;
DateTimeService = dateTimeService;
UserRepository = userRepository;
}
public async Task Register()
@@ -46,6 +49,12 @@ public class SessionService
};
SessionRepository.Add(OwnSession);
if (user != null) // Track last session init of user as last visited timestamp
{
user.LastVisitedAt = DateTimeService.GetCurrent();
UserRepository.Update(user);
}
}
public void Refresh()

View File

@@ -9,21 +9,36 @@ public class SmartDeployService
private readonly Repository<CloudPanel> CloudPanelRepository;
private readonly WebSpaceService WebSpaceService;
private readonly NodeService NodeService;
private readonly ConfigService ConfigService;
public SmartDeployService(
NodeRepository nodeRepository,
NodeService nodeService,
WebSpaceService webSpaceService,
Repository<CloudPanel> cloudPanelRepository)
Repository<CloudPanel> cloudPanelRepository,
ConfigService configService)
{
NodeRepository = nodeRepository;
NodeService = nodeService;
WebSpaceService = webSpaceService;
CloudPanelRepository = cloudPanelRepository;
ConfigService = configService;
}
public async Task<Node?> GetNode()
{
var config = ConfigService
.GetSection("Moonlight")
.GetSection("SmartDeploy")
.GetSection("Server");
if (config.GetValue<bool>("EnableOverride"))
{
var nodeId = config.GetValue<int>("OverrideNode");
return NodeRepository.Get().FirstOrDefault(x => x.Id == nodeId);
}
var data = new Dictionary<Node, double>();
foreach (var node in NodeRepository.Get().ToArray())
@@ -59,17 +74,17 @@ public class SmartDeployService
try
{
var cpuStats = await NodeService.GetCpuStats(node);
var memoryStats = await NodeService.GetMemoryStats(node);
var diskStats = await NodeService.GetDiskStats(node);
var cpuMetrics = await NodeService.GetCpuMetrics(node);
var memoryMetrics = await NodeService.GetMemoryMetrics(node);
var diskMetrics = await NodeService.GetDiskMetrics(node);
var cpuWeight = 0.5; // Weight of CPU usage in the final score
var memoryWeight = 0.3; // Weight of memory usage in the final score
var diskSpaceWeight = 0.2; // Weight of free disk space in the final score
var cpuScore = (1 - cpuStats.Usage) * cpuWeight; // CPU score is based on the inverse of CPU usage
var memoryScore = (1 - (memoryStats.Used / 1024)) * memoryWeight; // Memory score is based on the percentage of free memory
var diskSpaceScore = (double) diskStats.FreeBytes / 1000000000 * diskSpaceWeight; // Disk space score is based on the amount of free disk space in GB
var cpuScore = (1 - cpuMetrics.CpuUsage) * cpuWeight; // CPU score is based on the inverse of CPU usage
var memoryScore = (1 - (memoryMetrics.Used / 1024)) * memoryWeight; // Memory score is based on the percentage of free memory
var diskSpaceScore = (double) (diskMetrics.Total - diskMetrics.Used) / 1000000000 * diskSpaceWeight; // Disk space score is based on the amount of free disk space in GB
var finalScore = cpuScore + memoryScore + diskSpaceScore;

View File

@@ -7,20 +7,33 @@ namespace Moonlight.App.Services.Statistics;
public class StatisticsViewService
{
private readonly StatisticsRepository StatisticsRepository;
private readonly Repository<User> UserRepository;
private readonly DateTimeService DateTimeService;
public StatisticsViewService(StatisticsRepository statisticsRepository, DateTimeService dateTimeService)
public StatisticsViewService(StatisticsRepository statisticsRepository, DateTimeService dateTimeService, Repository<User> userRepository)
{
StatisticsRepository = statisticsRepository;
DateTimeService = dateTimeService;
UserRepository = userRepository;
}
public StatisticsData[] GetData(string chart, StatisticsTimeSpan timeSpan)
{
var startDate = DateTimeService.GetCurrent() - TimeSpan.FromHours((int)timeSpan);
var objs = StatisticsRepository.Get().Where(x => x.Date > startDate && x.Chart == chart);
var objs = StatisticsRepository
.Get()
.Where(x => x.Date > startDate && x.Chart == chart);
return objs.ToArray();
}
public int GetActiveUsers(StatisticsTimeSpan timeSpan)
{
var startDate = DateTimeService.GetCurrent() - TimeSpan.FromHours((int)timeSpan);
return UserRepository
.Get()
.Count(x => x.LastVisitedAt > startDate);
}
}

View File

@@ -72,11 +72,21 @@ public class WebSpaceService
public async Task Delete(WebSpace w)
{
var website = EnsureData(w);
var webSpace = WebSpaceRepository
.Get()
.Include(x => x.Databases)
.Include(x => x.CloudPanel)
.Include(x => x.Owner)
.First(x => x.Id == w.Id);
await CloudPanelApiHelper.Delete(website.CloudPanel, $"site/{website.Domain}", null);
foreach (var database in webSpace.Databases.ToArray())
{
await DeleteDatabase(webSpace, database);
}
WebSpaceRepository.Delete(website);
await CloudPanelApiHelper.Delete(webSpace.CloudPanel, $"site/{webSpace.Domain}", null);
WebSpaceRepository.Delete(webSpace);
}
public async Task<bool> IsHostUp(CloudPanel cloudPanel)

View File

@@ -9,6 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="6.0.5" />
<PackageReference Include="Blazor-ApexCharts" Version="0.9.16-beta" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
@@ -23,6 +24,7 @@
<PackageReference Include="FluentFTP" Version="46.0.2" />
<PackageReference Include="GravatarSharp.Core" Version="1.0.1.2" />
<PackageReference Include="JWT" Version="10.0.2" />
<PackageReference Include="LibGit2Sharp" Version="0.27.2" />
<PackageReference Include="Logging.Net" Version="1.1.3" />
<PackageReference Include="MailKit" Version="4.0.0" />
<PackageReference Include="Mappy.Net" Version="1.0.2" />
@@ -39,9 +41,9 @@
<PackageReference Include="MineStat" Version="3.1.1" />
<PackageReference Include="MySqlBackup.NET" Version="2.3.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3-beta1" />
<PackageReference Include="Octokit" Version="6.0.0" />
<PackageReference Include="Otp.NET" Version="1.3.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
<PackageReference Include="PteroConsole.NET" Version="1.0.4" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="RestSharp" Version="109.0.0-preview.1" />
<PackageReference Include="SSH.NET" Version="2020.0.2" />
@@ -72,9 +74,9 @@
<ItemGroup>
<Folder Include="App\ApiClients\CloudPanel\Resources\" />
<Folder Include="App\ApiClients\Daemon\Requests\" />
<Folder Include="App\Http\Middleware" />
<Folder Include="storage\backups\" />
<Folder Include="storage\resources\public\background\" />
</ItemGroup>
</Project>

View File

@@ -7,7 +7,6 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@inject ConfigService ConfigService
@inject BundleService BundleService
@inject LoadingMessageRepository LoadingMessageRepository
@{
@@ -38,29 +37,20 @@
<link rel="shortcut icon" href="@(moonlightConfig.GetValue<string>("AppUrl"))/api/moonlight/resources/images/logo.svg"/>
@*This import is not in the bundle because the files it references are linked relative to the current lath*@
<link rel="stylesheet" type="text/css" href="/assets/css/boxicons.min.css"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700"/>
@if (BundleService.BundledFinished)
{
<link rel="stylesheet" type="text/css" href="/api/moonlight/resources/bundle/css?idontwannabecached=@(BundleService.CacheId)"/>
}
else
{
foreach (var cssFile in BundleService.CssFiles)
{
if (cssFile.StartsWith("http"))
{
<link rel="stylesheet" type="text/css" href="@(cssFile)">
}
else
{
<style>
@cssFile
</style>
}
}
}
<link rel="stylesheet" type="text/css" href="/assets/css/style.bundle.css"/>
<link rel="stylesheet" type="text/css" href="/assets/css/flashbang.css"/>
<link rel="stylesheet" type="text/css" href="/assets/css/snow.css"/>
<link rel="stylesheet" type="text/css" href="/assets/css/utils.css"/>
<link rel="stylesheet" type="text/css" href="/assets/css/boxicons.min.css"/>
<link rel="stylesheet" type="text/css" href="/assets/css/blazor.css"/>
<link rel="stylesheet" type="text/css" href="/_content/XtermBlazor/XtermBlazor.css"/>
<link rel="stylesheet" type="text/css" href="/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.css"/>
<link rel="stylesheet" type="text/css" href="/_content/Blazor.ContextMenu/blazorContextMenu.min.css"/>
<link href="/assets/plugins/global/plugins.bundle.css" rel="stylesheet" type="text/css"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<base href="~/"/>
@@ -106,24 +96,33 @@
</div>
</div>
@if (BundleService.BundledFinished)
{
<script src="/api/moonlight/resources/bundle/js?idontwannabecached=@(BundleService.CacheId)">
</script>
}
else
{
foreach (var jsFile in BundleService.JsFiles)
{
if (jsFile.StartsWith("http"))
{
<script src="@(jsFile)"></script>
}
else
{
@Html.Raw("<script>" + jsFile +"</script>")
}
}
}
<script src="/_framework/blazor.server.js"></script>
<script src="/assets/plugins/global/plugins.bundle.js"></script>
<script src="/_content/XtermBlazor/XtermBlazor.min.js"></script>
<script src="/_content/BlazorTable/BlazorTable.min.js"></script>
<script src="/_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js"></script>
<script src="/_content/Blazor.ContextMenu/blazorContextMenu.min.js"></script>
<script src="https://www.google.com/recaptcha/api.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-search@0.8.2/lib/xterm-addon-search.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.5.0/lib/xterm-addon-web-links.min.js"></script>
<script src="/_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"></script>
<script>require.config({ paths: { 'vs': '/_content/BlazorMonaco/lib/monaco-editor/min/vs' } });</script>
<script src="/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js"></script>
<script src="/_content/BlazorMonaco/jsInterop.js"></script>
<script src="/assets/js/scripts.bundle.js"></script>
<script src="/assets/js/moonlight.js"></script>
<script>
moonlight.loading.registerXterm();
</script>
<script src="_content/Blazor-ApexCharts/js/apex-charts.min.js"></script>
<script src="_content/Blazor-ApexCharts/js/blazor-apex-charts.js"></script>
</body>
</html>

View File

@@ -1,14 +1,17 @@
using BlazorDownloadFile;
using BlazorTable;
using CurrieTechnologies.Razor.SweetAlert2;
using HealthChecks.UI.Client;
using Logging.Net;
using Moonlight.App.ApiClients.CloudPanel;
using Moonlight.App.ApiClients.Daemon;
using Moonlight.App.ApiClients.Paper;
using Moonlight.App.ApiClients.Wings;
using Moonlight.App.Database;
using Moonlight.App.Diagnostics.HealthChecks;
using Moonlight.App.Events;
using Moonlight.App.Helpers;
using Moonlight.App.Helpers.Wings;
using Moonlight.App.LogMigrator;
using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Domains;
@@ -31,9 +34,6 @@ namespace Moonlight
{
public class Program
{
// App version. Change for release
public static readonly string AppVersion = $"InDev {Formatter.FormatDateOnly(DateTime.Now.Date)}";
public static async Task Main(string[] args)
{
Logger.UsedLogger = new CacheLogger();
@@ -65,6 +65,10 @@ namespace Moonlight
options.HandshakeTimeout = TimeSpan.FromSeconds(10);
});
builder.Services.AddHttpContextAccessor();
builder.Services.AddHealthChecks()
.AddCheck<DatabaseHealthCheck>("Database")
.AddCheck<NodeHealthCheck>("Nodes")
.AddCheck<DaemonHealthCheck>("Daemons");
// Databases
builder.Services.AddDbContext<DataContext>();
@@ -127,7 +131,7 @@ namespace Moonlight
builder.Services.AddScoped<ReCaptchaService>();
builder.Services.AddScoped<IpBanService>();
builder.Services.AddSingleton<OAuth2Service>();
builder.Services.AddSingleton<BundleService>();
builder.Services.AddScoped<DynamicBackgroundService>();
builder.Services.AddScoped<SubscriptionService>();
builder.Services.AddScoped<SubscriptionAdminService>();
@@ -162,6 +166,9 @@ namespace Moonlight
builder.Services.AddSingleton<DiscordNotificationService>();
builder.Services.AddSingleton<CleanupService>();
// Other
builder.Services.AddSingleton<MoonlightService>();
// Third party services
builder.Services.AddBlazorTable();
builder.Services.AddSweetAlert2(options => { options.Theme = SweetAlertTheme.Dark; });
@@ -186,6 +193,10 @@ namespace Moonlight
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.MapHealthChecks("/_health", new()
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
// AutoStart services
_ = app.Services.GetRequiredService<CleanupService>();
@@ -193,6 +204,8 @@ namespace Moonlight
_ = app.Services.GetRequiredService<StatisticsCaptureService>();
_ = app.Services.GetRequiredService<DiscordNotificationService>();
_ = app.Services.GetRequiredService<MoonlightService>();
// Discord bot service
//var discordBotService = app.Services.GetRequiredService<DiscordBotService>();

View File

@@ -1,18 +1,11 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:26548",
"sslPort": 44339
}
},
"profiles": {
"Moonlight": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
"ASPNETCORE_ENVIRONMENT": "Development",
"ML_DEBUG": "true"
},
"applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
"dotnetRunMessages": true
@@ -22,7 +15,8 @@
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ML_SQL_DEBUG": "true"
"ML_SQL_DEBUG": "true",
"ML_DEBUG": "true"
},
"applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
"dotnetRunMessages": true
@@ -32,17 +26,11 @@
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ML_SQL_DEBUG": "true"
"ML_SQL_DEBUG": "true",
"ML_DEBUG": "true"
},
"applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
"dotnetRunMessages": true
},
"Docker": {
"commandName": "Docker",
"launchBrowser": false,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"publishAllPorts": true,
"useSSL": true
}
}
}

View File

@@ -185,6 +185,8 @@ else
await ToastService.Info(SmartTranslateService.Translate("Starting download"));
}
}
else
await ToastService.Error(SmartTranslateService.Translate("You are not able to download folders using the moonlight file manager"));
}
});

View File

@@ -31,12 +31,17 @@
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/profile/security">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/profile/discord">
<TL>Discord</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/profile/security">
<TL>Security</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/profile/subscriptions">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 3 ? "active" : "")" href="/profile/subscriptions">
<TL>Subscriptions</TL>
</a>
</li>

View File

@@ -0,0 +1,59 @@
@using Moonlight.App.Models.Misc
@using System.Text
@using Moonlight.App.Helpers
@{
string GetStatusColor(string s)
{
if (s == "Healthy")
return "success";
else if (s == "Unhealthy")
return "danger";
else
return "warning";
}
}
<div class="card">
<div class="card-header">
<div class="card-title">
<TL>Moonlight health</TL>:
<div class="ps-3 text-@(GetStatusColor(HealthCheck.Status))">
<TL>@(HealthCheck.Status)</TL>
</div>
</div>
</div>
<div class="card-body">
<div class="accordion" id="healthCheck">
@foreach (var entry in HealthCheck.Entries)
{
<div class="accordion-item">
<h2 class="accordion-header" id="healthCheck_1_header_@(entry.Key.ToLower())">
<button class="accordion-button fs-4 fw-semibold text-@(GetStatusColor(entry.Value.Status))" type="button" data-bs-toggle="collapse" data-bs-target="#healthCheck_body_@(entry.Key.ToLower())">
@(entry.Key)
</button>
</h2>
<div id="healthCheck_body_@(entry.Key.ToLower())" class="accordion-collapse collapse" data-bs-parent="#healthCheck">
<div class="accordion-body">
<b><TL>Status</TL>:</b>&nbsp;<TL>@(entry.Value.Status)</TL><br/>
<b><TL>Description</TL>:</b>&nbsp;@(entry.Value.Description)<br/>
<br/>
@foreach (var x in entry.Value.Data)
{
<b>@(x.Key)</b>
<br/>
@(x.Value)<br/>
}
</div>
</div>
</div>
}
</div>
</div>
</div>
@code
{
[Parameter]
public HealthCheck HealthCheck { get; set; }
}

View File

@@ -1,6 +1,4 @@
@using PteroConsole.NET
@using Moonlight.App.Services
@using Task = System.Threading.Tasks.Task
@using Moonlight.App.Services
@using Moonlight.App.Helpers
@using Logging.Net
@using BlazorContextMenu
@@ -101,9 +99,6 @@
@code
{
[CascadingParameter]
public PteroConsole Console { get; set; }
[CascadingParameter]
public Server CurrentServer { get; set; }

View File

@@ -1,11 +1,9 @@
@using PteroConsole.NET
@using PteroConsole.NET.Enums
@using Task = System.Threading.Tasks.Task
@using Moonlight.App.Helpers
@using Moonlight.App.Helpers
@using Moonlight.App.Repositories
@using Moonlight.App.Services
@using Logging.Net
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers.Wings
@using Moonlight.App.Helpers.Wings.Data
@using Moonlight.App.Services.Interop
@using Moonlight.Shared.Components.Xterm
@@ -37,7 +35,7 @@
@code
{
[CascadingParameter]
public PteroConsole Console { get; set; }
public WingsConsole Console { get; set; }
[CascadingParameter]
public Server CurrentServer { get; set; }
@@ -51,13 +49,26 @@
Console.OnMessage += OnMessage;
}
private async void OnMessage(object? sender, string e)
private async void OnMessage(object? sender, ConsoleMessage message)
{
if (Terminal != null)
{
var s = e;
if (message.IsInternal)
{
await Terminal.WriteLine("\x1b[38;5;16;48;5;135m\x1b[39m\x1b[1m Moonlight \x1b[0m " + message.Content + "\x1b[0m");
}
else
{
var s = message.Content;
s = s.Replace("Pterodactyl Daemon", "Moonlight Daemon");
if (s.Contains("Moonlight Daemon") || s.Contains("Pterodactyl Daemon"))
{
s = s.Replace("[39m", "\x1b[0m");
s = s.Replace("[33m", "[38;5;16;48;5;135m\x1b[39m");
}
s = s.Replace("[Pterodactyl Daemon]:", " Moonlight ");
s = s.Replace("[Moonlight Daemon]:", " Moonlight ");
s = s.Replace("Checking server disk space usage, this could take a few seconds...", TranslationService.Translate("Checking disk space"));
s = s.Replace("Updating process configuration files...", TranslationService.Translate("Updating config files"));
s = s.Replace("Ensuring file permissions are set correctly, this could take a few seconds...", TranslationService.Translate("Checking file permissions"));
@@ -68,6 +79,7 @@
await Terminal.WriteLine(s);
}
}
}
public void Dispose()
{
@@ -85,9 +97,9 @@
private void RunOnFirstRender()
{
lock (Console.MessageCache)
lock (Console.Messages)
{
foreach (var message in Console.MessageCache.TakeLast(30))
foreach (var message in Console.Messages)
{
OnMessage(null, message);
}

View File

@@ -4,6 +4,7 @@
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Services
@using Moonlight.App.ApiClients.Wings
@using Moonlight.App.Helpers.Wings
@inject WingsApiHelper WingsApiHelper
@inject WingsJwtHelper WingsJwtHelper

View File

@@ -1,9 +1,8 @@
@using PteroConsole.NET
@using PteroConsole.NET.Enums
@using Task = System.Threading.Tasks.Task
@using Moonlight.App.Services
@using Moonlight.App.Services
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Helpers.Wings
@using Moonlight.App.Helpers.Wings.Enums
@inject SmartTranslateService TranslationService
@@ -77,32 +76,32 @@
case ServerState.Starting:
<span class="text-warning"><TL>Starting</TL></span>
<span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.ServerResource.Uptime)))</span>
<span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.Resource.Uptime)))</span>
break;
case ServerState.Stopping:
<span class="text-warning"><TL>Stopping</TL></span>
<span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.ServerResource.Uptime)))</span>
<span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.Resource.Uptime)))</span>
break;
case ServerState.Running:
<span class="text-success"><TL>Online</TL></span>
<span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.ServerResource.Uptime)))</span>
<span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.Resource.Uptime)))</span>
break;
}
</span>
</div>
<div class="col fs-5">
<span class="fw-bold"><TL>Cpu</TL>:</span>
<span class="ms-1 text-muted">@(Math.Round(Console.ServerResource.CpuAbsolute, 2))%</span>
<span class="ms-1 text-muted">@(Math.Round(Console.Resource.CpuAbsolute / (CurrentServer.Cpu / 100f), 2))%</span>
</div>
<div class="col fs-5">
<span class="fw-bold"><TL>Memory</TL>:</span>
<span class="ms-1 text-muted">@(Formatter.FormatSize(Console.ServerResource.MemoryBytes)) / @(Formatter.FormatSize(Console.ServerResource.MemoryLimitBytes))</span>
<span class="ms-1 text-muted">@(Formatter.FormatSize(Console.Resource.MemoryBytes)) / @(Formatter.FormatSize(Console.Resource.MemoryLimitBytes))</span>
</div>
<div class="col fs-5">
<span class="fw-bold"><TL>Disk</TL>:</span>
<span class="ms-1 text-muted">@(Formatter.FormatSize(Console.ServerResource.DiskBytes)) / @(Math.Round(CurrentServer.Disk / 1024f, 2)) GB</span>
<span class="ms-1 text-muted">@(Formatter.FormatSize(Console.Resource.DiskBytes)) / @(Math.Round(CurrentServer.Disk / 1024f, 2)) GB</span>
</div>
</div>
</div>
@@ -178,7 +177,7 @@
public User User { get; set; }
[CascadingParameter]
public PteroConsole Console { get; set; }
public WingsConsole Console { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
@@ -190,8 +189,8 @@
protected override void OnInitialized()
{
Console.OnServerStateUpdated += async (sender, state) => { await InvokeAsync(StateHasChanged); };
Console.OnServerResourceUpdated += async (sender, x) => { await InvokeAsync(StateHasChanged); };
Console.OnServerStateUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); };
Console.OnResourceUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); };
}
#region Power Actions

View File

@@ -5,9 +5,11 @@
@using Moonlight.App.Repositories
@using Moonlight.App.Repositories.Servers
@using Logging.Net
@using Moonlight.App.ApiClients.Wings
@using Moonlight.App.Database.Entities
@inject ServerRepository ServerRepository
@inject ServerService ServerService
@inject SmartTranslateService TranslationService
<div class="col">
@@ -28,7 +30,8 @@
OnClick="Save"
Text="@(TranslationService.Translate("Change"))"
WorkingText="@(TranslationService.Translate("Changing"))"
CssClasses="btn-primary"></WButton>
CssClasses="btn-primary">
</WButton>
</td>
</tr>
</table>
@@ -58,6 +61,20 @@
ServerRepository.Update(CurrentServer);
var details = await ServerService.GetDetails(CurrentServer);
// For better user experience, we start the j2s server right away when the user enables j2s
if (details.State == "offline")
{
await ServerService.SetPowerState(CurrentServer, PowerSignal.Start);
}
// For better user experience, we kill the j2s server right away when the user disables j2s and the server is starting
if (details.State == "starting")
{
await ServerService.SetPowerState(CurrentServer, PowerSignal.Kill);
}
await Loader.Reload();
}
}

View File

@@ -10,7 +10,12 @@
<div class="col-xl-4 mb-xl-10">
<div class="card h-md-100">
<div class="card-body d-flex flex-column flex-center">
<img class="img-fluid" src="https://shs.moonlightpanel.xyz/api/screenshot?url=http://@(CurrentWebSpace.Domain)" alt="Website screenshot"/>
<div class="position-relative" style="width: 100%; height: 0; padding-bottom: 56.25%;">
<div class="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center">
<img class="img-fluid" src="/assets/media/gif/loading.gif" alt="Placeholder" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover;">
<div class="position-absolute top-0 start-0 w-100 h-100" style="background-image: url('https://shs.moonlightpanel.xyz/api/screenshot?url=http://@(CurrentWebSpace.Domain)'); background-size: cover; background-position: center;"></div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,9 +1,16 @@
@using Moonlight.App.Database.Entities
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@inject SmartTranslateService SmartTranslateService
@inject WebSpaceService WebSpaceService
@inject NavigationManager NavigationManager
@inject AlertService AlertService
<div class="card card-body">
<div class="row">
<div class="col-8">
<div class="d-flex align-items-center">
<div class="d-flex justify-content-between">
<div class="d-flex">
<div class="symbol symbol-circle me-5">
<div class="symbol-label bg-transparent text-primary border border-secondary border-dashed">
<i class="bx bx-globe bx-md"></i>
@@ -14,13 +21,17 @@
<div class="text-muted fs-5">@(WebSpace.CloudPanel.Name)</div>
</div>
</div>
<WButton Text="@(SmartTranslateService.Translate("Delete"))"
WorkingText="@(SmartTranslateService.Translate("Deleting"))"
OnClick="Delete"
CssClasses="btn-danger">
</WButton>
</div>
</div>
</div>
<div class="row">
<div class="separator my-5"></div>
</div>
<div class="card mb-5 mb-xl-10">
<div class="my-5"></div>
<div class="card mb-xl-10 mb-5">
<div class="card-body pt-0 pb-0">
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
<li class="nav-item mt-2">
@@ -54,4 +65,13 @@
[Parameter]
public WebSpace WebSpace { get; set; }
private async Task Delete()
{
if (await AlertService.ConfirmMath())
{
await WebSpaceService.Delete(WebSpace);
NavigationManager.NavigateTo("/webspaces");
}
}
}

View File

@@ -22,7 +22,7 @@
</label>
</div>
<div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="21">
<input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="22">
</div>
</div>
<div class="row fv-row mb-7">

View File

@@ -20,6 +20,7 @@
@inject ToastService ToastService
@inject SmartTranslateService SmartTranslateService
@inject IpBanService IpBanService
@inject DynamicBackgroundService DynamicBackgroundService
<GlobalErrorBoundary>
@{
@@ -56,7 +57,7 @@
<Sidebar></Sidebar>
<div class="app-main flex-column flex-row-fluid" id="kt_app_main">
<div class="d-flex flex-column flex-column-fluid">
<div id="kt_app_content" class="app-content flex-column-fluid">
<div id="kt_app_content" class="app-content flex-column-fluid" style="background-position: center; background-size: cover; background-repeat: no-repeat; background-attachment: fixed; background-image: url('@(DynamicBackgroundService.BackgroundImageUrl)')">
<div id="kt_app_content_container" class="app-container container-fluid">
<div class="mt-10">
<SoftErrorBoundary>
@@ -189,6 +190,11 @@
{
try
{
DynamicBackgroundService.OnBackgroundImageChanged += async (_, _) =>
{
await InvokeAsync(StateHasChanged);
};
IsIpBanned = await IpBanService.IsBanned();
if(IsIpBanned)
@@ -211,7 +217,13 @@
await SessionService.Register();
NavigationManager.LocationChanged += (sender, args) => { SessionService.Refresh(); };
NavigationManager.LocationChanged += async (_, _) =>
{
SessionService.Refresh();
if (!NavigationManager.Uri.Contains("/server/"))
await DynamicBackgroundService.Reset();
};
if (User != null)
{

View File

@@ -3,11 +3,16 @@
@using Moonlight.App.Repositories
@using Moonlight.App.Repositories.Domains
@using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Misc
@using Moonlight.App.Services
@using Newtonsoft.Json
@using Logging.Net
@inject ServerRepository ServerRepository
@inject UserRepository UserRepository
@inject Repository<WebSpace> WebSpaceRepository
@inject DomainRepository DomainRepository
@inject ConfigService ConfigService
<OnlyAdmin>
<LazyLoader Load="Load">
@@ -97,6 +102,28 @@
</a>
</div>
</div>
<LazyLoader Load="LoadHealthCheckData">
@if (HealthCheckData == null)
{
<div class="card">
<div class="card-header">
<div class="card-title">
<TL>Moonlight health</TL>
</div>
</div>
<div class="card-body">
<div class="alert alert-warning">
<TL>Unable to fetch health check data</TL>
</div>
</div>
</div>
}
else
{
<HealthCheckView HealthCheck="@HealthCheckData"/>
}
</LazyLoader>
</LazyLoader>
</OnlyAdmin>
@@ -107,6 +134,8 @@
private int DomainCount = 0;
private int WebSpaceCount = 0;
private HealthCheck? HealthCheckData;
private Task Load(LazyLoader lazyLoader)
{
ServerCount = ServerRepository.Get().Count();
@@ -116,4 +145,26 @@
return Task.CompletedTask;
}
private async Task LoadHealthCheckData(LazyLoader lazyLoader)
{
await lazyLoader.SetText("Loading health check data");
var appUrl = ConfigService
.GetSection("Moonlight")
.GetValue<string>("AppUrl");
try
{
using var client = new HttpClient();
var json = await client.GetStringAsync($"{appUrl}/_health");
HealthCheckData = JsonConvert.DeserializeObject<HealthCheck>(json) ?? new();
}
catch (Exception e)
{
HealthCheckData = null;
Logger.Warn("Unable to fetch health check data");
Logger.Warn(e);
}
}
}

View File

@@ -41,7 +41,7 @@ else
</div>
<div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (CpuStats == null)
@if (CpuMetrics == null)
{
<span class="text-muted">
<TL>Loading</TL>
@@ -50,12 +50,12 @@ else
else
{
<span>
@(CpuStats.Usage)% <TL>of</TL> @(CpuStats.Cores) <TL>Cores used</TL>
@(CpuMetrics.CpuUsage)% <TL>of CPU used</TL>
</span>
}
</span>
<span class="fw-semibold fs-6">
@if (CpuStats == null)
@if (CpuMetrics == null)
{
<span class="text-muted">
<TL>Loading</TL>
@@ -63,7 +63,7 @@ else
}
else
{
<span>@(CpuStats.Model)</span>
<span>@(CpuMetrics.CpuModel)</span>
}
</span>
</div>
@@ -78,7 +78,7 @@ else
</div>
<div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (MemoryStats == null)
@if (MemoryMetrics == null)
{
<span class="text-muted">
<TL>Loading</TL>
@@ -87,32 +87,12 @@ else
else
{
<span>
@(Formatter.FormatSize(MemoryStats.Used * 1024D * 1024D)) <TL>of</TL> @(Formatter.FormatSize(MemoryStats.Total * 1024D * 1024D)) <TL>used</TL>
@(Formatter.FormatSize(MemoryMetrics.Used)) <TL>of</TL> @(Formatter.FormatSize(MemoryMetrics.Total)) <TL>memory used</TL>
</span>
}
</span>
<span class="fw-semibold fs-6">
@if (MemoryStats == null)
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
if (MemoryStats.Sticks.Any())
{
foreach (var stick in SortMemorySticks(MemoryStats.Sticks))
{
<span>@(stick)</span>
<br/>
}
}
else
{
<span>No memory sticks detected</span>
}
}
@*IDK what to put here*@
</span>
</div>
</div>
@@ -126,7 +106,7 @@ else
</div>
<div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (DiskStats == null)
@if (DiskMetrics == null)
{
<span class="text-muted">
<TL>Loading</TL>
@@ -135,21 +115,12 @@ else
else
{
<span>
@(Formatter.FormatSize(DiskStats.TotalSize - DiskStats.FreeBytes)) <TL>of</TL> @(Formatter.FormatSize(DiskStats.TotalSize)) <TL>used</TL>
@(Formatter.FormatSize(DiskMetrics.Used)) <TL>of</TL> @(Formatter.FormatSize(DiskMetrics.Total)) <TL>used</TL>
</span>
}
</span>
<span class="fw-semibold fs-6">
@if (DiskStats == null)
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
<span>@(DiskStats.Name) - @(DiskStats.DriveFormat)</span>
}
@*IDK what to put here*@
</span>
</div>
</div>
@@ -239,7 +210,7 @@ else
</div>
<div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (ContainerStats == null)
@if (DockerMetrics == null)
{
<span class="text-muted">
<TL>Loading</TL>
@@ -248,12 +219,12 @@ else
else
{
<span>
<TL>@(ContainerStats.Containers.Count)</TL>
<TL>@(DockerMetrics.Containers.Length)</TL>
</span>
}
</span>
<span class="fw-semibold fs-6">
@if (ContainerStats == null)
@if (DockerMetrics == null)
{
<span class="text-muted">
<TL>Loading</TL>
@@ -290,11 +261,11 @@ else
private Node? Node;
private CpuStats CpuStats;
private MemoryStats MemoryStats;
private DiskStats DiskStats;
private CpuMetrics CpuMetrics;
private MemoryMetrics MemoryMetrics;
private DiskMetrics DiskMetrics;
private DockerMetrics DockerMetrics;
private SystemStatus SystemStatus;
private ContainerStats ContainerStats;
private async Task Load(LazyLoader arg)
{
@@ -311,16 +282,16 @@ else
SystemStatus = await NodeService.GetStatus(Node);
await InvokeAsync(StateHasChanged);
CpuStats = await NodeService.GetCpuStats(Node);
CpuMetrics = await NodeService.GetCpuMetrics(Node);
await InvokeAsync(StateHasChanged);
MemoryStats = await NodeService.GetMemoryStats(Node);
MemoryMetrics = await NodeService.GetMemoryMetrics(Node);
await InvokeAsync(StateHasChanged);
DiskStats = await NodeService.GetDiskStats(Node);
DiskMetrics = await NodeService.GetDiskMetrics(Node);
await InvokeAsync(StateHasChanged);
ContainerStats = await NodeService.GetContainerStats(Node);
DockerMetrics = await NodeService.GetDockerMetrics(Node);
await InvokeAsync(StateHasChanged);
}
catch (Exception e)
@@ -330,28 +301,4 @@ else
});
}
}
private List<string> SortMemorySticks(List<MemoryStats.MemoryStick> sticks)
{
// Thank you ChatGPT <3
var groupedMemory = sticks.GroupBy(memory => new { memory.Type, memory.Size })
.Select(group => new
{
Type = group.Key.Type,
Size = group.Key.Size,
Count = group.Count()
});
var sortedMemory = groupedMemory.OrderBy(memory => memory.Type)
.ThenBy(memory => memory.Size);
List<string> sortedList = sortedMemory.Select(memory =>
{
string sizeString = $"{memory.Size}GB";
return $"{memory.Count}x {memory.Type} {sizeString}";
}).ToList();
return sortedList;
}
}

View File

@@ -13,16 +13,16 @@
@inject FileDownloadService FileDownloadService
<OnlyAdmin>
<div class="row">
<LazyLoader @ref="LazyLoader" Load="Load">
@if (Image == null)
{
<div class="row">
<LazyLoader @ref="LazyLoader" Load="Load">
@if (Image == null)
{
<div class="alert alert-danger">
<TL>No image with this id found</TL>
</div>
}
else
{
}
else
{
<div class="row">
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="card card-body">
@@ -38,6 +38,16 @@
</label>
<textarea @bind="Image.Description" type="text" class="form-control"></textarea>
</div>
<div class="mb-10">
<label class="form-label">
<TL>Background image url</TL>
</label>
<input
@bind="Image.BackgroundImageUrl"
type="text"
class="form-control"
placeholder="@(SmartTranslateService.Translate("Leave empty for the default background image"))">
</div>
</div>
</div>
<div class="col-xl-6 mb-5 mb-xl-10">
@@ -244,9 +254,9 @@
</div>
</div>
</div>
}
</LazyLoader>
</div>
}
</LazyLoader>
</div>
</OnlyAdmin>
@code

View File

@@ -132,9 +132,9 @@
try
{
var containerStats = await NodeService.GetContainerStats(node);
var dockerMetrics = await NodeService.GetDockerMetrics(node);
foreach (var container in containerStats.Containers)
foreach (var container in dockerMetrics.Containers)
{
if (Guid.TryParse(container.Name, out Guid uuid))
{

View File

@@ -66,6 +66,20 @@
}
</div>
}
<div class="row">
<div class="col-sm-6">
<div class="card mt-4">
<div class="card-header">
<div class="card-title">
<TL>Active users</TL>
</div>
</div>
<div class="card-body">
<span class="fs-2">@(ActiveUsers)</span>
</div>
</div>
</div>
</div>
</LazyLoader>
</OnlyAdmin>
@@ -73,7 +87,9 @@
{
private StatisticsTimeSpan StatisticsTimeSpan = StatisticsTimeSpan.Day;
private LazyLoader Loader;
private Dictionary<string, StatisticsData[]> Charts = new();
private int ActiveUsers = 0;
private int TimeSpanBind
{
@@ -91,34 +107,48 @@
Charts.Add(
SmartTranslateService.Translate("Servers"),
AvgHelper.Calculate(
StatisticsViewService.GetData("serversCount", StatisticsTimeSpan)
)
);
Charts.Add(
SmartTranslateService.Translate("Users"),
AvgHelper.Calculate(
StatisticsViewService.GetData("usersCount", StatisticsTimeSpan)
)
);
Charts.Add(
SmartTranslateService.Translate("Domains"),
AvgHelper.Calculate(
StatisticsViewService.GetData("domainsCount", StatisticsTimeSpan)
)
);
Charts.Add(
SmartTranslateService.Translate("Databases"),
AvgHelper.Calculate(
StatisticsViewService.GetData("databasesCount", StatisticsTimeSpan)
)
);
Charts.Add(
SmartTranslateService.Translate("Webspaces"),
AvgHelper.Calculate(
StatisticsViewService.GetData("webspacesCount", StatisticsTimeSpan)
)
);
Charts.Add(
SmartTranslateService.Translate("Sessions"),
AvgHelper.Calculate(
StatisticsViewService.GetData("sessionsCount", StatisticsTimeSpan)
)
);
ActiveUsers = StatisticsViewService.GetActiveUsers(StatisticsTimeSpan);
return Task.CompletedTask;
}

View File

@@ -2,12 +2,15 @@
@using Moonlight.Shared.Components.Navigations
@using Moonlight.App.Helpers
@using Moonlight.App.Services
@inject HostSystemHelper HostSystemHelper
@inject MoonlightService MoonlightService
<OnlyAdmin>
<AdminSystemNavigation Index="0"/>
<LazyLoader Load="Load">
<div class="row">
<div class="col-xxl-6 my-3">
<div class="card">
@@ -19,7 +22,7 @@
<div class="card-body">
<span class="fs-5">
<TL>You are running moonlight version</TL>
<span class="text-primary">@(Program.AppVersion)</span>
<span class="text-primary">@(MoonlightService.AppVersion)</span>
</span>
</div>
</div>
@@ -70,5 +73,31 @@
</div>
</div>
</div>
<div class="col-xxl-6 my-3">
<div class="card">
<div class="card-header">
<span class="card-title">
<TL>Uptime</TL>
</span>
</div>
<div class="card-body">
<span class="fs-5">
<TL>Moonlight is since</TL>
<span class="text-primary">
@(Formatter.FormatUptime(DateTime.UtcNow - MoonlightService.StartTimestamp))
</span>
</span>
</div>
</div>
</div>
</div>
</LazyLoader>
</OnlyAdmin>
@code
{
private Task Load(LazyLoader arg)
{
return Task.CompletedTask;
}
}

View File

@@ -1,275 +1,16 @@
@page "/changelog"
@using Moonlight.App.Services
@inject MoonlightService MoonlightService
@{
List<string[]> changelog = new List<string[]>
{
new[]
{
"Patch 1, 13.03.2023",
"Service manager"
},
new[]
{
"Patch 2, 14.03.2023",
"Added new image manager. CRUD implemeted"
},
new[]
{
"Patch 3, 20.03.2023",
"Ddos detection",
"Implemented server manager"
},
new[]
{
"Patch 4, 21.03.2023",
"Added user edit form. Fixed edit link"
},
new[]
{
"Patch 5, 24.03.2023",
"Update Discord Bot Branche",
"Removed discord id and discriminator. Fixed oauth2.",
"Updated discord bot branch",
"Updated smart form branch",
"Added smart form",
"Updating PleskIntegration branch",
"Revert \"Updating PleskIntegration branch\""
},
new[]
{
"Patch 6, 27.03.2023",
"Update Index.razor",
"User settings"
},
new[]
{
"Patch 7, 28.03.2023",
"Added form proccessing screen (not finished). Some ui changes. User m."
},
new[]
{
"Patch 8, 02.04.2023",
"Server manage enhancements",
"Cleanup system",
"Update WingsServerConverter.cs",
"Added quick create dropdown",
"fixed login form",
"Login form fix"
},
new[]
{
"Patch 9, 03.04.2023",
"Fixed cleanup",
"Added server reset setting",
"Totp",
"Update from upstream because of database models",
"Audit log",
"Audit log",
"Force password change",
"Support chat redesign",
"Subscriptions",
"Added server rename setting"
},
new[]
{
"Patch 10, 04.04.2023",
"Added server delete. Tweaked setting names",
"Added server node status screen check thingy",
"Update CacheLogger.cs",
"Update to upstream branch",
"Update View.razor",
"Update to upstream branch",
"Removed legacy aaPanel stuff",
"Update DesignFixes",
"Design fixes",
"Create Changelog.razor"
},
new[]
{
"Patch 11, 05.04.2023",
"Ftp file manager",
"Update to upstream branch",
"Added ActivityStatus"
},
new[]
{
"Patch 12, 06.04.2023",
"Domain overview",
"Plesk integration",
"Add websites to statistics branch",
"Removed legacy database ui",
"Replaced legacy dependency resolve with server service function for w.",
"masu is too stupid to use ulong",
"Statistic system",
"host file access + use in admin resource manager"
},
new[]
{
"Patch 13, 10.04.2023",
"Removed old typeahead. Added own solution. Lang file, notes",
"Implemented website order"
},
new[]
{
"Patch 14, 11.04.2023",
"Implemented domain order. Fixed some bugs"
},
new[]
{
"Patch 15, 12.04.2023",
"Implemented new soft error boundary crash handler",
"Added ip locate. Fixed oauth2 mail send. Fixed mail send when generat.",
"Implemented multi allocation",
"register",
"Fixed server lists",
"News page",
"Improved news system"
},
new[]
{
"Patch 16, 13.04.2023",
"domain view redesigned | fixed some masu at midnight",
"Persistent storage",
"Removed old file manager stuff",
"fixed design of delete button",
"redesigned shared domains screen | split in view/add"
},
new[]
{
"Patch 17, 14.04.2023",
"Removed legacy image tags",
"Fixed server deletion and creation allocation bugs"
},
new[]
{
"Patch 18, 15.04.2023",
"Update DiscordBot branch",
"Fix backup delete",
"Implemented default subscription",
"Implemented random loading message",
"Recode frontend js"
},
new[]
{
"Patch 19, 16.04.2023",
"Optimized allocation search. Added sql command log interception",
"Fixed my stupid mistakes",
"Implemented file view loading animation"
},
new[]
{
"Patch 20, 19.04.2023",
"Cloud panel interation"
},
new[]
{
"Patch 21, 20.04.2023",
"Removed legacy website",
"Added user change status try catch, implement logout. password change",
"Added admin check to sftp login system",
"Implemented new event system",
"Rewritten support chat backend. Added discord notifications"
},
new[]
{
"Patch 22, 21.04.2023",
"Removed old support system",
"Deleted old message system. Replaced it with new event system",
"Update ServerNavigation.razor",
"Switched from internal smtp client to MailKit. Added ssl config"
},
new[]
{
"Patch 23, 23.04.2023",
"Added missing refresh call",
"Added better reconnect screens",
"Removed useless logging. Switched to cache mode for support chat",
"Fixed statistics system"
},
new[]
{
"Patch 24, 24.04.2023",
"Add checking user status to login sequence where login form is currently",
"Added config reload button"
},
new[]
{
"Patch 25, 25.04.2023",
"Update Create.razor",
"Added server count for image overview",
"Implemented owner change option in admin ui"
},
new[]
{
"Patch 26, 26.04.2023",
"Image import export",
"Fixed import and export",
"Prevent message duplication with sender check",
"Implemented forge version switcher"
},
new[]
{
"Patch 27, 28.04.2023",
"Improved user details profile page",
"Implemented fabric version setting"
},
new[]
{
"Patch 28, 29.04.2023",
"update my DiscordBot branch",
"Revert \"update my DiscordBot branch\"",
"Discord bot",
"Update to upstream branch"
},
new[]
{
"Patch 29, 02.05.2023",
"Update Index.razor",
"Added multi line feature and file upload in the support chat"
},
new[]
{
"Patch 30, 04.05.2023",
"Implemented new rating system"
},
new[]
{
"Patch 31, 05.05.2023",
"Code cleanup",
"added version and main jar setting",
"fixed server settings design",
"dotnet runtime settings"
},
new[]
{
"Patch 32, 06.05.2023",
"file create"
},
new[]
{
"Patch 33, 17.05.2023",
"Added node selector for server create screen",
"Added website directory limit for website file manager",
"Added new screenhot service",
"Added node and image view to manager. Made the data handling smoother"
},
new[]
{
"Patch 34, 19.05.2023",
"Added recaptcha. Added recaptcha to register page",
"Implemented ip ban"
}
};
changelog.Reverse();
int i = 0;
}
<div class="card card-docs flex-row-fluid mb-2">
<div class="card-body fs-6 py-15 px-10 py-lg-15 px-lg-15 text-gray-700">
<div class="accordion accordion-flush accordion-icon-toggle" id="kt_accordion">
@foreach (var prt in changelog)
@foreach (var prt in MoonlightService.ChangeLog.ToArray().Reverse())
{
i++;
<div class="accordion-item mb-5">

View File

@@ -1,5 +1,4 @@
@page "/domain/{Id:int}"
@using Moonlight.App.Services.Sessions
@using Moonlight.App.Repositories.Domains
@using Moonlight.App.Database.Entities
@using Microsoft.EntityFrameworkCore
@@ -11,6 +10,8 @@
@inject DomainRepository DomainRepository
@inject DomainService DomainService
@inject SmartTranslateService SmartTranslateService
@inject NavigationManager NavigationManager
@inject AlertService AlertService
<LazyLoader Load="Load">
@if (Domain == null)
@@ -28,6 +29,13 @@
<span class="card-title">
<TL>DNS records for</TL><span class="ms-3">@(domainNameBuilt)</span>
</span>
<div class="card-toolbar">
<WButton Text="@(SmartTranslateService.Translate("Delete domain"))"
WorkingText="@(SmartTranslateService.Translate("Deleting"))"
CssClasses="btn-danger"
OnClick="Delete">
</WButton>
</div>
</div>
<div class="mt-5"></div>
<LazyLoader @ref="DnsLazyLoader" Load="LoadDnsRecords">
@@ -240,4 +248,13 @@
await DomainService.DeleteDnsRecord(Domain!, record);
await DnsLazyLoader.Reload();
}
private async Task Delete()
{
if (await AlertService.ConfirmMath())
{
await DomainService.Delete(Domain!);
NavigationManager.NavigateTo("/domains");
}
}
}

View File

@@ -125,8 +125,9 @@
</div>
</div>
<!--d-flex flex-row mb-5-->
<div class="card mb-5">
<div class="row">
<div class="col">
<div class="card mb-5">
<div class="card-header card-header-stretch">
<div class="card-title d-flex align-items-center">
<h3 class="fw-bold m-0 text-gray-800">
@@ -173,15 +174,16 @@
<TL>Create a domain</TL>
</a>
<span class="text-gray-400 fw-semibold d-block fs-6">
<TL>Make your servvices accessible throught your own domain</TL>
<TL>Make your services accessible through your own domain</TL>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-5">
</div>
</div>
<div class="col">
<div class="card mb-5">
<div class="card-header card-header-stretch">
<div class="card-title d-flex align-items-center">
<h3 class="fw-bold m-0 text-gray-800">
@@ -234,6 +236,8 @@
</div>
</div>
</div>
</div>
</div>
</div>
</LazyLoader>

View File

@@ -0,0 +1,77 @@
@page "/profile/discord"
@using Moonlight.Shared.Components.Navigations
@using Moonlight.App.Database.Entities
@using Moonlight.App.Repositories
@using Moonlight.App.Services
@inject Repository<User> UserRepository
@inject SmartTranslateService SmartTranslateService
<ProfileNavigation Index="1"/>
@if (User.DiscordId == 0)
{
<div class="row">
<div class="col">
<div class="alert bg-warning d-flex flex-column flex-sm-row p-5 mb-10">
<i class="fs-2hx text-light me-4 mb-5 mb-sm-0 bx bx-error bx-lg"></i>
<div class="d-flex flex-column text-light pe-0 pe-sm-10">
<h4 class="mb-2 light">
<TL>Your account is currently not linked to discord</TL>
</h4>
<TL>To use features like the discord bot, link your moonlight account with your discord account</TL><br/>
</div>
</div>
</div>
<div class="col">
<div class="card card-body">
<a class="btn btn-primary" href="/api/moonlight/oauth2/discord/start">
<TL>Link account</TL>
</a>
</div>
</div>
</div>
}
else
{
<div class="row">
<div class="col">
<div class="alert bg-success d-flex flex-column flex-sm-row p-5 mb-10">
<i class="fs-2hx text-light me-4 mb-5 mb-sm-0 bx bx-check bx-lg"></i>
<div class="d-flex flex-column text-light pe-0 pe-sm-10">
<h4 class="mb-2 light">
<TL>Your account is linked to a discord account</TL>
</h4>
<TL>You are able to use features like the discord bot of moonlight</TL>
</div>
</div>
</div>
<div class="col">
<div class="card card-body">
<WButton CssClasses="btn-danger"
Text="@(SmartTranslateService.Translate("Remove link"))"
WorkingText="@(SmartTranslateService.Translate("Working"))"
OnClick="RemoveLink">
</WButton>
</div>
</div>
</div>
}
@code
{
[CascadingParameter]
public User User { get; set; }
private async Task RemoveLink()
{
User.DiscordId = 0;
UserRepository.Update(User);
await InvokeAsync(StateHasChanged);
}
}

View File

@@ -19,7 +19,7 @@
@inject AlertService AlertService
@inject ToastService ToastService
<ProfileNavigation Index="1"/>
<ProfileNavigation Index="2"/>
<div class="card mb-5 mb-xl-10">
<LazyLoader Load="Load">

View File

@@ -11,7 +11,7 @@
@inject SubscriptionService SubscriptionService
@inject SmartTranslateService SmartTranslateService
<ProfileNavigation Index="2"/>
<ProfileNavigation Index="3"/>
<div class="card mb-3">
<div class="row g-0">

View File

@@ -1,15 +1,16 @@
@page "/server/{ServerUuid}/{Route?}"
@using PteroConsole.NET
@using Task = System.Threading.Tasks.Task
@using Moonlight.App.Repositories.Servers
@using PteroConsole.NET.Enums
@using Microsoft.EntityFrameworkCore
@using Logging.Net
@using Moonlight.App.Database.Entities
@using Moonlight.App.Events
@using Moonlight.App.Helpers
@using Moonlight.App.Helpers.Wings
@using Moonlight.App.Helpers.Wings.Enums
@using Moonlight.App.Repositories
@using Moonlight.App.Services
@using Moonlight.App.Services.Sessions
@using Moonlight.Shared.Components.Xterm
@using Moonlight.Shared.Components.ServerControl
@using Newtonsoft.Json
@@ -20,6 +21,7 @@
@inject EventSystem Event
@inject ServerService ServerService
@inject NavigationManager NavigationManager
@inject DynamicBackgroundService DynamicBackgroundService
@implements IDisposable
@@ -44,7 +46,7 @@
{
if (NodeOnline)
{
if (Console.ConnectionState == ConnectionState.Connected)
if (Console.ConsoleState == ConsoleState.Connected)
{
if (Console.ServerState == ServerState.Installing)
{
@@ -179,7 +181,7 @@
[Parameter]
public string? Route { get; set; }
private PteroConsole? Console;
private WingsConsole? Console;
private Server? CurrentServer;
private Node Node;
private bool NodeOnline = false;
@@ -193,11 +195,11 @@
{
Console = new();
Console.OnConnectionStateUpdated += (_, _) => { InvokeAsync(StateHasChanged); };
Console.OnServerResourceUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); };
Console.OnServerStateUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); };
Console.OnConsoleStateUpdated += (_, _) => { InvokeAsync(StateHasChanged); };
Console.OnResourceUpdated += (_, _) => { InvokeAsync(StateHasChanged); };
Console.OnServerStateUpdated += (_, _) => { InvokeAsync(StateHasChanged); };
Console.RequestToken += (_) => WingsConsoleHelper.GenerateToken(CurrentServer!);
Console.OnRequestNewToken += async _ => await WingsConsoleHelper.GenerateToken(CurrentServer!);
Console.OnMessage += async (_, s) =>
{
@@ -205,7 +207,10 @@
{
if (InstallConsole != null)
{
await InstallConsole.WriteLine(s);
if (s.IsInternal)
await InstallConsole.WriteLine("\x1b[38;5;16;48;5;135m\x1b[39m\x1b[1m Moonlight \x1b[0m " + s.Content + "\x1b[0m");
else
await InstallConsole.WriteLine(s.Content);
}
}
};
@@ -280,7 +285,7 @@
await lazyLoader.SetText("Connecting to console");
await WingsConsoleHelper.ConnectWings(Console!, CurrentServer);
await ReconnectConsole();
await Event.On<Server>($"server.{CurrentServer.Uuid}.installComplete", this, server =>
{
@@ -288,6 +293,11 @@
return Task.CompletedTask;
});
if (string.IsNullOrEmpty(Image.BackgroundImageUrl))
await DynamicBackgroundService.Reset();
else
await DynamicBackgroundService.Change(Image.BackgroundImageUrl);
}
}
else
@@ -296,11 +306,22 @@
}
}
private async Task ReconnectConsole()
{
await Console!.Disconnect();
await WingsConsoleHelper.ConnectWings(Console!, CurrentServer!);
}
public async void Dispose()
{
if (CurrentServer != null)
{
await Event.Off($"server.{CurrentServer.Uuid}.installComplete", this);
}
if (Console != null)
{
Console.Dispose();
}
}
}

View File

@@ -1,5 +1,4 @@
@page "/servers"
@using Moonlight.App.Services.Sessions
@using Moonlight.App.Repositories.Servers
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.Database.Entities
@@ -9,9 +8,97 @@
@inject IServiceScopeFactory ServiceScopeFactory
<LazyLoader Load="Load">
@if (AllServers.Any())
@if (AllServers.Any())
{
if (UseSortedServerView)
{
@foreach (var server in AllServers)
var groupedServers = AllServers
.OrderBy(x => x.Name)
.GroupBy(x => x.Image.Name);
foreach (var groupedServer in groupedServers)
{
<div class="separator separator-content my-15">@(groupedServer.Key)</div>
<div class="card card-body bg-secondary py-0 my-0 mx-0 px-0">
@foreach (var server in groupedServer)
{
<div class="row mx-4 my-4">
<a class="card card-body" href="/server/@(server.Uuid)">
<div class="row">
<div class="col">
<div class="d-flex align-items-center">
<div class="symbol symbol-50px me-3">
<i class="bx bx-md bx-server"></i>
</div>
<div class="d-flex justify-content-start flex-column">
<a href="/server/@(server.Uuid)" class="text-gray-800 text-hover-primary mb-1 fs-5">
@(server.Name)
</a>
<span class="text-gray-400 fw-semibold d-block fs-6">
@(Math.Round(server.Memory / 1024D, 2)) GB / @(Math.Round(server.Disk / 1024D, 2)) GB / @(server.Node.Name) <span class="text-gray-700">- @(server.Image.Name)</span>
</span>
</div>
</div>
</div>
<div class="d-none d-sm-block col my-auto fs-6">
@(server.Node.Fqdn):@(server.MainAllocation.Port)
</div>
<div class="d-none d-sm-block col my-auto fs-6">
@if (StatusCache.ContainsKey(server))
{
var status = StatusCache[server];
switch (status)
{
case "offline":
<span class="text-danger">
<TL>Offline</TL>
</span>
break;
case "stopping":
<span class="text-warning">
<TL>Stopping</TL>
</span>
break;
case "starting":
<span class="text-warning">
<TL>Starting</TL>
</span>
break;
case "running":
<span class="text-success">
<TL>Running</TL>
</span>
break;
case "failed":
<span class="text-gray-400">
<TL>Failed</TL>
</span>
break;
default:
<span class="text-danger">
<TL>Offline</TL>
</span>
break;
}
}
else
{
<span class="text-gray-400">
<TL>Loading</TL>
</span>
}
</div>
</div>
</a>
</div>
}
</div>
}
}
else
{
foreach (var server in AllServers)
{
<div class="row px-5 mb-5">
<a class="card card-body" href="/server/@(server.Uuid)">
@@ -42,28 +129,42 @@
switch (status)
{
case "offline":
<span class="text-danger"><TL>Offline</TL></span>
<span class="text-danger">
<TL>Offline</TL>
</span>
break;
case "stopping":
<span class="text-warning"><TL>Stopping</TL></span>
<span class="text-warning">
<TL>Stopping</TL>
</span>
break;
case "starting":
<span class="text-warning"><TL>Starting</TL></span>
<span class="text-warning">
<TL>Starting</TL>
</span>
break;
case "running":
<span class="text-success"><TL>Running</TL></span>
<span class="text-success">
<TL>Running</TL>
</span>
break;
case "failed":
<span class="text-gray-400"><TL>Failed</TL></span>
<span class="text-gray-400">
<TL>Failed</TL>
</span>
break;
default:
<span class="text-danger"><TL>Offline</TL></span>
<span class="text-danger">
<TL>Offline</TL>
</span>
break;
}
}
else
{
<span class="text-gray-400"><TL>Loading</TL></span>
<span class="text-gray-400">
<TL>Loading</TL>
</span>
}
</div>
</div>
@@ -71,8 +172,9 @@
</div>
}
}
else
{
}
else
{
<div class="alert bg-info d-flex flex-column flex-sm-row w-100 p-5">
<div class="d-flex flex-column pe-0 pe-sm-10">
<h4 class="fw-semibold">
@@ -83,7 +185,28 @@
</span>
</div>
</div>
}
}
<div class="row mt-7 px-3">
<div class="card">
<div class="card-header">
<div class="card-title">
<span class="badge badge-primary badge-lg">Beta</span>
</div>
</div>
<div class="card-body">
<div class="row">
<label class="col-lg-4 col-form-label fw-semibold fs-6">Sorted server view</label>
<div class="col-lg-8 d-flex align-items-center">
<div class="form-check form-check-solid form-switch form-check-custom fv-row">
<input class="form-check-input w-45px h-30px" type="checkbox" id="sortedServerView" @bind="UseSortedServerView">
<label class="form-check-label" for="sortedServerView"></label>
</div>
</div>
</div>
</div>
</div>
</div>
</LazyLoader>
@code
@@ -94,6 +217,8 @@
private Server[] AllServers;
private readonly Dictionary<Server, string> StatusCache = new();
private bool UseSortedServerView = false;
private Task Load(LazyLoader arg)
{
AllServers = ServerRepository
@@ -127,7 +252,7 @@
return Task.CompletedTask;
}
private void AddStatus(App.Database.Entities.Server server, string status)
private void AddStatus(Server server, string status)
{
lock (StatusCache)
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB