69 Commits
v1b3 ... v1b8

Author SHA1 Message Date
Marcel Baumgartner
432e441972 Change moonlight service to always fetch the changelog 2023-06-18 02:37:07 +02:00
Marcel Baumgartner
1dae5150bd Merge pull request #178 from Moonlight-Panel/RewriteNotificationSystem
Rewrite notification system
2023-06-18 02:32:33 +02:00
Marcel Baumgartner
72c6f636ee Rewritten notification system 2023-06-17 20:47:07 +02:00
Daniel Balk
e54d04277d Merge pull request #177 from Moonlight-Panel/ShowMoonlightAppInSessions
added check for moonlight app for getting the device
2023-06-17 17:52:41 +02:00
Daniel Balk
f559f08e8d added check for moonlight app for getting the device 2023-06-17 17:51:34 +02:00
Daniel Balk
2674fb3fa7 Update Debugging.razor 2023-06-17 14:10:24 +02:00
Marcel Baumgartner
d5d77ae7da Merge pull request #175 from Moonlight-Panel/AddMoonlightSideDnsCheckSsl
Added moonlight side dns check for ssl
2023-06-17 13:39:25 +02:00
Marcel Baumgartner
3ae905038c Added moonlight side dns check for ssl 2023-06-17 13:38:32 +02:00
Marcel Baumgartner
6707d722e0 Merge pull request #174 from Moonlight-Panel/SomeHotfixes
Fixed cleanup, error handling missing and missing gif
2023-06-17 13:00:12 +02:00
Marcel Baumgartner
bc9fecf6d9 Fixed cleanup, error handling missing and missing gif 2023-06-17 12:57:01 +02:00
Marcel Baumgartner
c2cb10f069 Merge pull request #173 from Moonlight-Panel/CleanupHotfix
Fixed cleanup calculation errors
2023-06-17 00:30:16 +02:00
Marcel Baumgartner
cc06d1eb0b Fixed cleanup calculation errors 2023-06-17 00:29:47 +02:00
Marcel Baumgartner
916ff71022 Merge pull request #172 from Moonlight-Panel/FixConsoleIssues
Fixed xterm console issues
2023-06-16 22:57:04 +02:00
Marcel Baumgartner
9e80342e26 Fixed xterm console issues 2023-06-16 22:56:33 +02:00
Marcel Baumgartner
0fb97683bf Merge pull request #171 from Moonlight-Panel/DisableServerDelete
Disabled server delete
2023-06-16 20:30:00 +02:00
Marcel Baumgartner
32415bad54 Merge pull request #170 from Moonlight-Panel/AddModrinthSupport
Add modrinth support
2023-06-16 20:29:45 +02:00
Marcel Baumgartner
d2b0bcc4a3 Disabled server delete 2023-06-16 20:29:23 +02:00
Daniel Balk
2f4f4193d3 Merge pull request #169 from Moonlight-Panel/NotificationDebuggingUi
added simple debugging page
2023-06-16 20:25:39 +02:00
Daniel Balk
7279f05a16 added simple debugging page 2023-06-16 20:24:32 +02:00
Marcel Baumgartner
3962723acb Implemented plugin installer 2023-06-16 20:24:03 +02:00
Marcel Baumgartner
125e72fa58 Switched to new routing for the server manage page 2023-06-16 17:43:48 +02:00
Marcel Baumgartner
880cce060f Added missing server installing db change 2023-06-16 17:33:47 +02:00
Marcel Baumgartner
0e04942111 Merge pull request #168 from Moonlight-Panel/AddServerArchive
Added archive system. Added ml debug menu and related stuff
2023-06-16 17:21:05 +02:00
Marcel Baumgartner
46a88d4638 Added archive system. Added ml debug menu and related stuff 2023-06-16 16:58:58 +02:00
Marcel Baumgartner
c7c39fc511 Merge pull request #167 from Moonlight-Panel/RemoveUselessConsoleLogs
Removed useless console streaming logs
2023-06-13 22:41:46 +02:00
Marcel Baumgartner
3b9bdd1916 Removed useless console streaming logs 2023-06-13 22:35:03 +02:00
Marcel Baumgartner
d267be6d69 Merge pull request #166 from Moonlight-Panel/AddServerFetchBotApi
Added server fetch for single server in bot api
2023-06-13 21:51:20 +02:00
Marcel Baumgartner
18f6a1acdc Added server fetch for single server in bot api 2023-06-13 21:50:33 +02:00
Marcel Baumgartner
2ca41ff18f Merge pull request #165 from Moonlight-Panel/FixSessionListEmailFilter
Fixed session list email filter
2023-06-12 22:12:37 +02:00
Marcel Baumgartner
74c77bc744 Fixed session list email filter 2023-06-12 22:12:05 +02:00
Marcel Baumgartner
1ff8cdd7a9 Merge pull request #164 from Moonlight-Panel/RemovedUnnecessaryReload
Removed unnecessary reload
2023-06-12 00:47:42 +02:00
Marcel Baumgartner
bd320d025a Removed unnecessary reload 2023-06-12 00:47:00 +02:00
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
127 changed files with 6451 additions and 1369 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,5 +1,4 @@
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Newtonsoft.Json; using Newtonsoft.Json;
using RestSharp; using RestSharp;
@@ -14,26 +13,13 @@ public class DaemonApiHelper
Client = new(); 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) 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.Method = Method.Get;
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", node.Token); var response = await Client.ExecuteAsync(request);
var response = await Client.GetAsync(request);
if (!response.IsSuccessful) if (!response.IsSuccessful)
{ {
@@ -52,4 +38,69 @@ public class DaemonApiHelper
return JsonConvert.DeserializeObject<T>(response.Content!)!; 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

@@ -0,0 +1,59 @@
using Logging.Net;
using Newtonsoft.Json;
using RestSharp;
namespace Moonlight.App.ApiClients.Modrinth;
public class ModrinthApiHelper
{
private readonly RestClient Client;
public ModrinthApiHelper()
{
Client = new();
Client.AddDefaultParameter(
new HeaderParameter("User-Agent", "Moonlight-Panel/Moonlight (admin@endelon-hosting.de)")
);
}
public async Task<T> Get<T>(string resource)
{
var request = CreateRequest(resource);
request.Method = Method.Get;
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
if (response.StatusCode != 0)
{
throw new ModrinthException(
$"An error occured: ({response.StatusCode}) {response.Content}",
(int)response.StatusCode
);
}
else
{
throw new Exception($"An internal error occured: {response.ErrorMessage}");
}
}
return JsonConvert.DeserializeObject<T>(response.Content!)!;
}
private RestRequest CreateRequest(string resource)
{
var url = "https://api.modrinth.com/v2/" + resource;
var request = new RestRequest(url)
{
Timeout = 60 * 15
};
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
return request;
}
}

View File

@@ -0,0 +1,19 @@
namespace Moonlight.App.ApiClients.Modrinth;
public class ModrinthException : Exception
{
public int StatusCode { get; set; }
public ModrinthException()
{
}
public ModrinthException(string message, int statusCode) : base(message)
{
StatusCode = statusCode;
}
public ModrinthException(string message, Exception inner) : base(message, inner)
{
}
}

View File

@@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace Moonlight.App.ApiClients.Modrinth.Resources;
public class Pagination
{
[JsonProperty("hits")] public Project[] Hits { get; set; }
[JsonProperty("offset")] public long Offset { get; set; }
[JsonProperty("limit")] public long Limit { get; set; }
[JsonProperty("total_hits")] public long TotalHits { get; set; }
}

View File

@@ -0,0 +1,48 @@
using Newtonsoft.Json;
namespace Moonlight.App.ApiClients.Modrinth.Resources;
public class Project
{
[JsonProperty("project_id")] public string ProjectId { get; set; }
[JsonProperty("project_type")] public string ProjectType { get; set; }
[JsonProperty("slug")] public string Slug { get; set; }
[JsonProperty("author")] public string Author { get; set; }
[JsonProperty("title")] public string Title { get; set; }
[JsonProperty("description")] public string Description { get; set; }
[JsonProperty("categories")] public string[] Categories { get; set; }
[JsonProperty("display_categories")] public string[] DisplayCategories { get; set; }
[JsonProperty("versions")] public string[] Versions { get; set; }
[JsonProperty("downloads")] public long Downloads { get; set; }
[JsonProperty("follows")] public long Follows { get; set; }
[JsonProperty("icon_url")] public string IconUrl { get; set; }
[JsonProperty("date_created")] public DateTimeOffset DateCreated { get; set; }
[JsonProperty("date_modified")] public DateTimeOffset DateModified { get; set; }
[JsonProperty("latest_version")] public string LatestVersion { get; set; }
[JsonProperty("license")] public string License { get; set; }
[JsonProperty("client_side")] public string ClientSide { get; set; }
[JsonProperty("server_side")] public string ServerSide { get; set; }
[JsonProperty("gallery")] public Uri[] Gallery { get; set; }
[JsonProperty("featured_gallery")] public Uri FeaturedGallery { get; set; }
[JsonProperty("color")] public long? Color { get; set; }
}

View File

@@ -0,0 +1,57 @@
using Newtonsoft.Json;
namespace Moonlight.App.ApiClients.Modrinth.Resources;
public class Version
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("version_number")]
public string VersionNumber { get; set; }
[JsonProperty("changelog")]
public string Changelog { get; set; }
[JsonProperty("dependencies")]
public object[] Dependencies { get; set; }
[JsonProperty("game_versions")]
public object[] GameVersions { get; set; }
[JsonProperty("version_type")]
public string VersionType { get; set; }
[JsonProperty("loaders")]
public object[] Loaders { get; set; }
[JsonProperty("featured")]
public bool Featured { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("requested_status")]
public string RequestedStatus { get; set; }
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("project_id")]
public string ProjectId { get; set; }
[JsonProperty("author_id")]
public string AuthorId { get; set; }
[JsonProperty("date_published")]
public DateTime DatePublished { get; set; }
[JsonProperty("downloads")]
public long Downloads { get; set; }
[JsonProperty("changelog_url")]
public object ChangelogUrl { get; set; }
[JsonProperty("files")]
public VersionFile[] Files { get; set; }
}

View File

@@ -0,0 +1,21 @@
using Newtonsoft.Json;
namespace Moonlight.App.ApiClients.Modrinth.Resources;
public class VersionFile
{
[JsonProperty("url")]
public Uri Url { get; set; }
[JsonProperty("filename")]
public string Filename { get; set; }
[JsonProperty("primary")]
public bool Primary { get; set; }
[JsonProperty("size")]
public long Size { get; set; }
[JsonProperty("file_type")]
public string FileType { get; set; }
}

View File

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

View File

@@ -13,6 +13,8 @@ public class Server
public string OverrideStartup { get; set; } = ""; public string OverrideStartup { get; set; } = "";
public bool Installing { get; set; } = false; public bool Installing { get; set; } = false;
public bool Suspended { get; set; } = false; public bool Suspended { get; set; } = false;
public bool IsArchived { get; set; } = false;
public ServerBackup? Archive { get; set; } = null;
public List<ServerVariable> Variables { get; set; } = new(); public List<ServerVariable> Variables { get; set; } = new();
public List<ServerBackup> Backups { get; set; } = new(); public List<ServerBackup> Backups { get; set; } = new();

View File

@@ -43,6 +43,7 @@ public class User
// Date stuff // Date stuff
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime LastVisitedAt { get; set; } = DateTime.UtcNow;
// Subscriptions // 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");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddedServerAchive : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "ArchiveId",
table: "Servers",
type: "int",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "IsArchived",
table: "Servers",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.CreateIndex(
name: "IX_Servers_ArchiveId",
table: "Servers",
column: "ArchiveId");
migrationBuilder.AddForeignKey(
name: "FK_Servers_ServerBackups_ArchiveId",
table: "Servers",
column: "ArchiveId",
principalTable: "ServerBackups",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Servers_ServerBackups_ArchiveId",
table: "Servers");
migrationBuilder.DropIndex(
name: "IX_Servers_ArchiveId",
table: "Servers");
migrationBuilder.DropColumn(
name: "ArchiveId",
table: "Servers");
migrationBuilder.DropColumn(
name: "IsArchived",
table: "Servers");
}
}
}

View File

@@ -132,6 +132,10 @@ namespace Moonlight.App.Database.Migrations
b.Property<int>("Allocations") b.Property<int>("Allocations")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("BackgroundImageUrl")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ConfigFiles") b.Property<string>("ConfigFiles")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
@@ -492,6 +496,9 @@ namespace Moonlight.App.Database.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("int"); .HasColumnType("int");
b.Property<int?>("ArchiveId")
.HasColumnType("int");
b.Property<int>("Cpu") b.Property<int>("Cpu")
.HasColumnType("int"); .HasColumnType("int");
@@ -507,6 +514,9 @@ namespace Moonlight.App.Database.Migrations
b.Property<bool>("Installing") b.Property<bool>("Installing")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
b.Property<bool>("IsArchived")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsCleanupException") b.Property<bool>("IsCleanupException")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
@@ -538,6 +548,8 @@ namespace Moonlight.App.Database.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ArchiveId");
b.HasIndex("ImageId"); b.HasIndex("ImageId");
b.HasIndex("MainAllocationId"); b.HasIndex("MainAllocationId");
@@ -758,6 +770,9 @@ namespace Moonlight.App.Database.Migrations
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<DateTime>("LastVisitedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Password") b.Property<string>("Password")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
@@ -928,6 +943,10 @@ namespace Moonlight.App.Database.Migrations
modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b =>
{ {
b.HasOne("Moonlight.App.Database.Entities.ServerBackup", "Archive")
.WithMany()
.HasForeignKey("ArchiveId");
b.HasOne("Moonlight.App.Database.Entities.Image", "Image") b.HasOne("Moonlight.App.Database.Entities.Image", "Image")
.WithMany() .WithMany()
.HasForeignKey("ImageId") .HasForeignKey("ImageId")
@@ -950,6 +969,8 @@ namespace Moonlight.App.Database.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Archive");
b.Navigation("Image"); b.Navigation("Image");
b.Navigation("MainAllocation"); b.Navigation("MainAllocation");

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

@@ -112,4 +112,22 @@ public class EventSystem
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task<T> WaitForEvent<T>(string id, object handle, Func<T, bool> filter)
{
var taskCompletionSource = new TaskCompletionSource<T>();
Func<T, Task> action = async data =>
{
if (filter.Invoke(data))
{
taskCompletionSource.SetResult(data);
await Off(id, handle);
}
};
On<T>(id, handle, action);
return taskCompletionSource.Task;
}
} }

View File

@@ -5,6 +5,8 @@ namespace Moonlight.App.Exceptions;
[Serializable] [Serializable]
public class DisplayException : Exception public class DisplayException : Exception
{ {
public bool DoNotTranslate { get; set; } = false;
// //
// For guidelines regarding the creation of new exception types, see // For guidelines regarding the creation of new exception types, see
// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp
@@ -19,6 +21,11 @@ public class DisplayException : Exception
public DisplayException(string message) : base(message) public DisplayException(string message) : base(message)
{ {
} }
public DisplayException(string message, bool doNotTranslate) : base(message)
{
DoNotTranslate = doNotTranslate;
}
public DisplayException(string message, Exception inner) : base(message, inner) public DisplayException(string message, Exception inner) : base(message, inner)
{ {

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

@@ -17,6 +17,18 @@ public static class Formatter
return $"{t.Hours}h {t.Minutes}m {t.Seconds}s"; 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) private static double Round(this double d, int decimals)
{ {
@@ -116,4 +128,12 @@ public static class Formatter
return (i / (1024D * 1024D)).Round(2) + " GB"; 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

@@ -243,6 +243,7 @@ public class WingsConsole : IDisposable
break; break;
} }
} }
catch(JsonReaderException){}
catch (Exception e) catch (Exception e)
{ {
if (!Disconnecting) if (!Disconnecting)

View File

@@ -102,7 +102,7 @@ public class DiscordBotController : Controller
return BadRequest(); return BadRequest();
} }
[HttpGet("{id}/servers/{uuid}")] [HttpGet("{id}/servers/{uuid}/details")]
public async Task<ActionResult<ServerDetails>> GetServerDetails(ulong id, Guid uuid) public async Task<ActionResult<ServerDetails>> GetServerDetails(ulong id, Guid uuid)
{ {
if (!await IsAuth(Request)) if (!await IsAuth(Request))
@@ -123,6 +123,33 @@ public class DiscordBotController : Controller
return await ServerService.GetDetails(server); return await ServerService.GetDetails(server);
} }
[HttpGet("{id}/servers/{uuid}")]
public async Task<ActionResult<ServerDetails>> GetServer(ulong id, Guid uuid)
{
if (!await IsAuth(Request))
return StatusCode(403);
var user = await GetUserFromDiscordId(id);
if (user == null)
return BadRequest();
var server = ServerRepository
.Get()
.Include(x => x.Owner)
.Include(x => x.Image)
.Include(x => x.Node)
.FirstOrDefault(x => x.Owner.Id == user.Id && x.Uuid == uuid);
if (server == null)
return NotFound();
server.Node.Token = "";
server.Node.TokenId = "";
return Ok(server);
}
private Task<User?> GetUserFromDiscordId(ulong discordId) private Task<User?> GetUserFromDiscordId(ulong discordId)
{ {

View File

@@ -1,7 +1,9 @@
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text; using System.Text;
using Logging.Net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Notification; using Moonlight.App.Database.Entities.Notification;
using Moonlight.App.Models.Notifications; using Moonlight.App.Models.Notifications;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
@@ -12,135 +14,156 @@ using Newtonsoft.Json;
namespace Moonlight.App.Http.Controllers.Api.Moonlight.Notifications; namespace Moonlight.App.Http.Controllers.Api.Moonlight.Notifications;
public class ListenController : ControllerBase [ApiController]
[Route("api/moonlight/notification/listen")]
public class ListenController : Controller
{ {
internal WebSocket ws; private WebSocket WebSocket;
private bool active = true;
private bool isAuth = false;
private NotificationClient Client; private NotificationClient Client;
private CancellationTokenSource CancellationTokenSource = new();
private readonly IdentityService IdentityService;
private readonly NotificationRepository NotificationRepository;
private readonly OneTimeJwtService OneTimeJwtService;
private readonly NotificationClientService NotificationClientService;
private readonly NotificationServerService NotificationServerService;
public ListenController(IdentityService identityService, private User? CurrentUser;
NotificationRepository notificationRepository,
OneTimeJwtService oneTimeJwtService, private readonly OneTimeJwtService OneTimeJwtService;
NotificationClientService notificationClientService, private readonly NotificationServerService NotificationServerService;
NotificationServerService notificationServerService) private readonly Repository<NotificationClient> NotificationClientRepository;
public ListenController(
OneTimeJwtService oneTimeJwtService,
NotificationServerService notificationServerService, Repository<NotificationClient> notificationClientRepository)
{ {
IdentityService = identityService;
NotificationRepository = notificationRepository;
OneTimeJwtService = oneTimeJwtService; OneTimeJwtService = oneTimeJwtService;
NotificationClientService = notificationClientService;
NotificationServerService = notificationServerService; NotificationServerService = notificationServerService;
NotificationClientRepository = notificationClientRepository;
} }
[Route("/api/moonlight/notifications/listen")] [Route("/api/moonlight/notifications/listen")]
public async Task Get() public async Task<ActionResult> Get()
{ {
if (HttpContext.WebSockets.IsWebSocketRequest) if (HttpContext.WebSockets.IsWebSocketRequest)
{ {
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); WebSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
ws = webSocket;
await Echo(); await ProcessWebsocket();
return new EmptyResult();
} }
else else
{ {
HttpContext.Response.StatusCode = StatusCodes.Status403Forbidden; return StatusCode(400);
} }
} }
private async Task Echo() private async Task ProcessWebsocket()
{ {
while (active) while (!CancellationTokenSource.Token.IsCancellationRequested && WebSocket.State == WebSocketState.Open)
{ {
byte[] bytes = new byte[1024 * 16]; try
var asg = new ArraySegment<byte>(bytes);
var res = await ws.ReceiveAsync(asg, CancellationToken.None);
var text = Encoding.UTF8.GetString(bytes).Trim('\0');
var obj = JsonConvert.DeserializeObject<BasicWSModel>(text);
if (!string.IsNullOrWhiteSpace(obj.Action))
{ {
await HandleRequest(text, obj.Action); byte[] buffer = new byte[1024 * 16];
_ = await WebSocket.ReceiveAsync(buffer, CancellationTokenSource.Token);
var text = Encoding.UTF8.GetString(buffer).Trim('\0');
var basicWsModel = JsonConvert.DeserializeObject<BasicWSModel>(text) ?? new();
if (!string.IsNullOrWhiteSpace(basicWsModel.Action))
{
await HandleRequest(text, basicWsModel.Action);
}
if (WebSocket.State != WebSocketState.Open)
{
CancellationTokenSource.Cancel();
}
}
catch (WebSocketException e)
{
CancellationTokenSource.Cancel();
} }
active = ws.State == WebSocketState.Open;
} }
await NotificationServerService.UnRegisterClient(Client);
} }
private async Task HandleRequest(string text, string action) private async Task HandleRequest(string text, string action)
{ {
if (!isAuth && action == "login") if (CurrentUser == null && action != "login")
await Login(text);
else if (!isAuth)
await ws.SendAsync(Encoding.UTF8.GetBytes("{\"error\": \"Unauthorised\"}"), WebSocketMessageType.Text,
WebSocketMessageFlags.EndOfMessage, CancellationToken.None);
else switch (action)
{ {
await Send("{\"error\": \"Unauthorised\"}");
}
switch (action)
{
case "login":
await Login(text);
break;
case "received": case "received":
await Received(text); await Received(text);
break; break;
case "read": case "read":
await Read(text); await Read(text);
break; break;
default:
break;
} }
} }
private async Task Send(string text)
{
await WebSocket.SendAsync(
Encoding.UTF8.GetBytes(text),
WebSocketMessageType.Text,
WebSocketMessageFlags.EndOfMessage, CancellationTokenSource.Token
);
}
private async Task Login(string json) private async Task Login(string json)
{ {
var jwt = JsonConvert.DeserializeObject<Login>(json).token; var loginModel = JsonConvert.DeserializeObject<Login>(json) ?? new();
var dict = await OneTimeJwtService.Validate(jwt); var dict = await OneTimeJwtService.Validate(loginModel.Token);
if (dict == null) if (dict == null)
{ {
string error = "{\"status\":false}"; await Send("{\"status\":false}");
var bytes = Encoding.UTF8.GetBytes(error);
await ws.SendAsync(bytes, WebSocketMessageType.Text, WebSocketMessageFlags.EndOfMessage, CancellationToken.None);
return; return;
} }
var _clientId = dict["clientId"]; if (!int.TryParse(dict["clientId"], out int clientId))
var clientId = int.Parse(_clientId); {
await Send("{\"status\":false}");
return;
}
var client = NotificationRepository.GetClients().Include(x => x.User).First(x => x.Id == clientId); Client = NotificationClientRepository
.Get()
.Include(x => x.User)
.First(x => x.Id == clientId);
Client = client; CurrentUser = Client.User;
await InitWebsocket();
string success = "{\"status\":true}";
var byt = Encoding.UTF8.GetBytes(success);
await ws.SendAsync(byt, WebSocketMessageType.Text, WebSocketMessageFlags.EndOfMessage, CancellationToken.None);
}
private async Task InitWebsocket() await NotificationServerService.RegisterClient(WebSocket, Client);
{
NotificationClientService.listenController = this;
NotificationClientService.WebsocketReady(Client);
isAuth = true; await Send("{\"status\":true}");
} }
private async Task Received(string json) private async Task Received(string json)
{ {
var id = JsonConvert.DeserializeObject<NotificationById>(json).notification; var id = JsonConvert.DeserializeObject<NotificationById>(json).Notification;
//TODO: Implement ws notification received //TODO: Implement ws notification received
} }
private async Task Read(string json) private async Task Read(string json)
{ {
var id = JsonConvert.DeserializeObject<NotificationById>(json).notification; var model = JsonConvert.DeserializeObject<NotificationById>(json) ?? new();
await NotificationServerService.SendAction(NotificationClientService.User, await NotificationServerService.SendAction(
JsonConvert.SerializeObject(new NotificationById() {Action = "hide", notification = id})); CurrentUser!,
JsonConvert.SerializeObject(
new NotificationById()
{
Action = "hide", Notification = model.Notification
}
)
);
} }
} }

View File

@@ -16,14 +16,12 @@ public class ResourcesController : Controller
{ {
private readonly SecurityLogService SecurityLogService; private readonly SecurityLogService SecurityLogService;
private readonly BucketService BucketService; private readonly BucketService BucketService;
private readonly BundleService BundleService;
public ResourcesController(SecurityLogService securityLogService, public ResourcesController(SecurityLogService securityLogService,
BucketService bucketService, BundleService bundleService) BucketService bucketService)
{ {
SecurityLogService = securityLogService; SecurityLogService = securityLogService;
BucketService = bucketService; BucketService = bucketService;
BundleService = bundleService;
} }
[HttpGet("images/{name}")] [HttpGet("images/{name}")]
@@ -48,6 +46,29 @@ public class ResourcesController : Controller
return NotFound(); 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}")] [HttpGet("bucket/{bucket}/{name}")]
public async Task<ActionResult> GetBucket([FromRoute] string bucket, [FromRoute] string name) public async Task<ActionResult> GetBucket([FromRoute] string bucket, [FromRoute] string name)
@@ -77,34 +98,4 @@ public class ResourcesController : Controller
return Problem(); 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

@@ -0,0 +1,8 @@
namespace Moonlight.App.Models.Forms;
public class ServerImageDataModel
{
public string OverrideStartup { get; set; }
public int DockerImageIndex { get; set; }
}

View File

@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
using Moonlight.App.Database.Entities;
namespace Moonlight.App.Models.Forms;
public class ServerOverviewDataModel
{
[Required(ErrorMessage = "You need to enter a name")]
[MaxLength(32, ErrorMessage = "The name cannot be longer that 32 characters")]
public string Name { get; set; }
[Required(ErrorMessage = "You need to specify a owner")]
public User Owner { get; set; }
}

View File

@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
public class ServerResourcesDataModel
{
[Required(ErrorMessage = "You need to specify the cpu cores")]
public int Cpu { get; set; }
[Required(ErrorMessage = "You need to specify the memory")]
public long Memory { get; set; }
[Required(ErrorMessage = "You need to specify the disk")]
public long Disk { get; set; }
}

View File

@@ -0,0 +1,19 @@
using System.Net.WebSockets;
using System.Text;
using Moonlight.App.Database.Entities.Notification;
namespace Moonlight.App.Models.Misc;
public class ActiveNotificationClient
{
public WebSocket WebSocket { get; set; }
public NotificationClient Client { get; set; }
public async Task SendAction(string action)
{
await WebSocket.SendAsync(
Encoding.UTF8.GetBytes(action),
WebSocketMessageType.Text,
WebSocketMessageFlags.EndOfMessage, CancellationToken.None);
}
}

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 class RunningServer
{ {
public Server Server { get; set; } public Server Server { get; set; }
public ContainerStats.Container Container { get; set; } public Container Container { get; set; }
} }

View File

@@ -1,6 +1,8 @@
namespace Moonlight.App.Models.Notifications; using Newtonsoft.Json;
namespace Moonlight.App.Models.Notifications;
public class Login : BasicWSModel public class Login : BasicWSModel
{ {
public string token { get; set; } [JsonProperty("token")] public string Token { get; set; } = "";
} }

View File

@@ -1,6 +1,9 @@
namespace Moonlight.App.Models.Notifications; using Newtonsoft.Json;
namespace Moonlight.App.Models.Notifications;
public class NotificationById : BasicWSModel public class NotificationById : BasicWSModel
{ {
public int notification { get; set; } [JsonProperty("notification")]
public int Notification { get; set; }
} }

View File

@@ -0,0 +1,83 @@
using Moonlight.App.ApiClients.Modrinth;
using Moonlight.App.ApiClients.Modrinth.Resources;
using Moonlight.App.Exceptions;
using FileAccess = Moonlight.App.Helpers.Files.FileAccess;
using Version = Moonlight.App.ApiClients.Modrinth.Resources.Version;
namespace Moonlight.App.Services.Addon;
public class ServerAddonPluginService
{
private readonly ModrinthApiHelper ModrinthApiHelper;
private readonly ServerService ServerService;
public ServerAddonPluginService(ModrinthApiHelper modrinthApiHelper)
{
ModrinthApiHelper = modrinthApiHelper;
}
public async Task<Project[]> GetPluginsForVersion(string version, string search = "")
{
string resource;
var filter =
"[[\"categories:\'bukkit\'\",\"categories:\'paper\'\",\"categories:\'spigot\'\"],[\"versions:" + version + "\"],[\"project_type:mod\"]]";
if (string.IsNullOrEmpty(search))
resource = "search?limit=21&index=relevance&facets=" + filter;
else
resource = $"search?query={search}&limit=21&index=relevance&facets=" + filter;
var result = await ModrinthApiHelper.Get<Pagination>(resource);
return result.Hits;
}
public async Task InstallPlugin(FileAccess fileAccess, string version, Project project, Action<string>? onStateUpdated = null)
{
// Resolve plugin download
onStateUpdated?.Invoke($"Resolving {project.Slug}");
var filter = "game_versions=[\"" + version + "\"]&loaders=[\"bukkit\", \"paper\", \"spigot\"]";
var versions = await ModrinthApiHelper.Get<Version[]>(
$"project/{project.Slug}/version?" + filter);
if (!versions.Any())
throw new DisplayException("No plugin download for your minecraft version found");
var installVersion = versions.OrderByDescending(x => x.DatePublished).First();
var fileToInstall = installVersion.Files.First();
// Download plugin in a stream cached mode
var httpClient = new HttpClient();
var stream = await httpClient.GetStreamAsync(fileToInstall.Url);
var dataStream = new MemoryStream(1024 * 1024 * 40);
await stream.CopyToAsync(dataStream);
stream.Close();
dataStream.Position = 0;
// Install plugin
await fileAccess.SetDir("/");
try
{
await fileAccess.MkDir("plugins");
}
catch (Exception)
{
// Ignored
}
await fileAccess.SetDir("plugins");
onStateUpdated?.Invoke($"Installing {project.Slug}");
await fileAccess.Upload(fileToInstall.Filename, dataStream);
await dataStream.DisposeAsync();
//TODO: At some point of time, create a dependency resolver
}
}

View File

@@ -5,6 +5,7 @@ using Moonlight.App.ApiClients.Daemon.Resources;
using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Events; using Moonlight.App.Events;
using Moonlight.App.Helpers;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -81,12 +82,35 @@ public class CleanupService
{ {
try try
{ {
var cpuStats = await nodeService.GetCpuStats(node); var cpuMetrics = await nodeService.GetCpuMetrics(node);
var memoryStats = await nodeService.GetMemoryStats(node); var memoryMetrics = await nodeService.GetMemoryMetrics(node);
if (cpuStats.Usage > maxCpu || memoryStats.Free < minMemory) bool executeCleanup;
if (cpuMetrics.CpuUsage > maxCpu)
{ {
var containerStats = await nodeService.GetContainerStats(node); Logger.Debug($"{node.Name}: CPU Usage is too high");
Logger.Debug($"Usage: {cpuMetrics.CpuUsage}");
Logger.Debug($"Max CPU: {maxCpu}");
executeCleanup = true;
}
else if (Formatter.BytesToGb(memoryMetrics.Total) - Formatter.BytesToGb(memoryMetrics.Used) <
minMemory / 1024D)
{
Logger.Debug($"{node.Name}: Memory Usage is too high");
Logger.Debug($"Memory (Total): {Formatter.BytesToGb(memoryMetrics.Total)}");
Logger.Debug($"Memory (Used): {Formatter.BytesToGb(memoryMetrics.Used)}");
Logger.Debug(
$"Memory (Free): {Formatter.BytesToGb(memoryMetrics.Total) - Formatter.BytesToGb(memoryMetrics.Used)}");
Logger.Debug($"Min Memory: {minMemory}");
executeCleanup = true;
}
else
executeCleanup = false;
if (executeCleanup)
{
var dockerMetrics = await nodeService.GetDockerMetrics(node);
var serverRepository = scope.ServiceProvider.GetRequiredService<ServerRepository>(); var serverRepository = scope.ServiceProvider.GetRequiredService<ServerRepository>();
var imageRepository = scope.ServiceProvider.GetRequiredService<ImageRepository>(); var imageRepository = scope.ServiceProvider.GetRequiredService<ImageRepository>();
@@ -101,9 +125,9 @@ public class CleanupService
) )
.ToArray(); .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)) if (Guid.TryParse(container.Name, out Guid uuid))
{ {
@@ -139,6 +163,7 @@ public class CleanupService
if (players == 0) if (players == 0)
{ {
Logger.Debug($"Restarted {server.Name} ({server.Uuid}) on node {node.Name}");
await serverService.SetPowerState(server, PowerSignal.Restart); await serverService.SetPowerState(server, PowerSignal.Restart);
ServersCleaned++; ServersCleaned++;
@@ -164,10 +189,12 @@ public class CleanupService
if (handleJ2S) if (handleJ2S)
{ {
Logger.Debug($"Restarted (cleanup) {server.Name} ({server.Uuid}) on node {node.Name}");
await serverService.SetPowerState(server, PowerSignal.Restart); await serverService.SetPowerState(server, PowerSignal.Restart);
} }
else else
{ {
Logger.Debug($"Stopped {server.Name} ({server.Uuid}) on node {node.Name}");
await serverService.SetPowerState(server, PowerSignal.Stop); await serverService.SetPowerState(server, PowerSignal.Stop);
} }

View File

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

View File

@@ -0,0 +1,98 @@
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 (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;
using Moonlight.App.ApiClients.Daemon.Requests;
using Moonlight.App.ApiClients.Daemon.Resources; using Moonlight.App.ApiClients.Daemon.Resources;
using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings;
using Moonlight.App.ApiClients.Wings.Resources; using Moonlight.App.ApiClients.Wings.Resources;
@@ -24,35 +25,56 @@ public class NodeService
return await WingsApiHelper.Get<SystemStatus>(node, "api/system"); 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) public async Task<bool> IsHostUp(Node node)
{ {
try try
{ {
//TODO: Implement status caching await GetStatus(node);
var data = await GetStatus(node);
if (data != null) return true;
return true;
} }
catch (Exception) catch (Exception)
{ {

View File

@@ -1,43 +0,0 @@
using System.Net.WebSockets;
using System.Text;
using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Notification;
using Moonlight.App.Http.Controllers.Api.Moonlight.Notifications;
using Moonlight.App.Repositories;
using Moonlight.App.Services.Sessions;
namespace Moonlight.App.Services.Notifications;
public class NotificationClientService
{
private readonly NotificationRepository NotificationRepository;
private readonly NotificationServerService NotificationServerService;
internal ListenController listenController;
public NotificationClientService(NotificationRepository notificationRepository, NotificationServerService notificationServerService)
{
NotificationRepository = notificationRepository;
NotificationServerService = notificationServerService;
}
public User User => NotificationClient.User;
public NotificationClient NotificationClient { get; set; }
public async Task SendAction(string action)
{
await listenController.ws.SendAsync(Encoding.UTF8.GetBytes(action), WebSocketMessageType.Text,
WebSocketMessageFlags.EndOfMessage, CancellationToken.None);
}
public void WebsocketReady(NotificationClient client)
{
NotificationClient = client;
NotificationServerService.AddClient(this);
}
public void WebsocketClosed()
{
NotificationServerService.RemoveClient(this);
}
}

View File

@@ -1,79 +1,113 @@
using Microsoft.EntityFrameworkCore; using System.Net.WebSockets;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Notification; using Moonlight.App.Database.Entities.Notification;
using Moonlight.App.Events;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
namespace Moonlight.App.Services.Notifications; namespace Moonlight.App.Services.Notifications;
public class NotificationServerService public class NotificationServerService
{ {
private UserRepository UserRepository; private readonly List<ActiveNotificationClient> ActiveClients = new();
private NotificationRepository NotificationRepository;
private readonly IServiceScopeFactory ServiceScopeFactory; private readonly IServiceScopeFactory ServiceScopeFactory;
private IServiceScope ServiceScope; private readonly EventSystem Event;
public NotificationServerService(IServiceScopeFactory serviceScopeFactory) public NotificationServerService(IServiceScopeFactory serviceScopeFactory, EventSystem eventSystem)
{ {
ServiceScopeFactory = serviceScopeFactory; ServiceScopeFactory = serviceScopeFactory;
Task.Run(Run); Event = eventSystem;
}
private Task Run()
{
ServiceScope = ServiceScopeFactory.CreateScope();
UserRepository = ServiceScope
.ServiceProvider
.GetRequiredService<UserRepository>();
NotificationRepository = ServiceScope
.ServiceProvider
.GetRequiredService<NotificationRepository>();
return Task.CompletedTask;
} }
private List<NotificationClientService> connectedClients = new(); public Task<ActiveNotificationClient[]> GetActiveClients()
public List<NotificationClientService> GetConnectedClients(User user)
{ {
return connectedClients.Where(x => x.User == user).ToList(); lock (ActiveClients)
{
return Task.FromResult(ActiveClients.ToArray());
}
}
public Task<ActiveNotificationClient[]> GetUserClients(User user)
{
lock (ActiveClients)
{
return Task.FromResult(
ActiveClients
.Where(x => x.Client.User.Id == user.Id)
.ToArray()
);
}
} }
public async Task SendAction(User user, string action) public async Task SendAction(User user, string action)
{ {
var clients = NotificationRepository.GetClients().Include(x => x.User).Where(x => x.User == user).ToList(); using var scope = ServiceScopeFactory.CreateScope();
var notificationClientRepository =
scope.ServiceProvider.GetRequiredService<Repository<NotificationClient>>();
var clients = notificationClientRepository
.Get()
.Include(x => x.User)
.Where(x => x.User == user)
.ToList();
foreach (var client in clients) foreach (var client in clients)
{ {
var notificationAction = new NotificationAction() ActiveNotificationClient[] connectedUserClients;
{
Action = action,
NotificationClient = client
};
var connected = connectedClients.Where(x => x.NotificationClient.Id == client.Id).ToList();
if (connected.Count > 0) lock (ActiveClients)
{ {
var clientService = connected[0]; connectedUserClients = ActiveClients
await clientService.SendAction(action); .Where(x => x.Client.Id == user.Id)
.ToArray();
}
if (connectedUserClients.Length > 0)
{
await connectedUserClients[0].SendAction(action);
} }
else else
{ {
NotificationRepository.AddAction(notificationAction); var notificationAction = new NotificationAction()
{
Action = action,
NotificationClient = client
};
var notificationActionsRepository =
scope.ServiceProvider.GetRequiredService<Repository<NotificationAction>>();
notificationActionsRepository.Add(notificationAction);
} }
} }
} }
public void AddClient(NotificationClientService notificationClientService) public async Task RegisterClient(WebSocket webSocket, NotificationClient notificationClient)
{ {
connectedClients.Add(notificationClientService); var newClient = new ActiveNotificationClient()
{
WebSocket = webSocket,
Client = notificationClient
};
lock (ActiveClients)
{
ActiveClients.Add(newClient);
}
await Event.Emit("notifications.addClient", notificationClient);
} }
public void RemoveClient(NotificationClientService notificationClientService) public async Task UnRegisterClient(NotificationClient client)
{ {
connectedClients.Remove(notificationClientService); lock (ActiveClients)
{
ActiveClients.RemoveAll(x => x.Client == client);
}
await Event.Emit("notifications.removeClient", client);
} }
} }

View File

@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore; using Logging.Net;
using Microsoft.EntityFrameworkCore;
using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings;
using Moonlight.App.ApiClients.Wings.Requests; using Moonlight.App.ApiClients.Wings.Requests;
using Moonlight.App.ApiClients.Wings.Resources; using Moonlight.App.ApiClients.Wings.Resources;
@@ -19,6 +20,7 @@ namespace Moonlight.App.Services;
public class ServerService public class ServerService
{ {
private readonly Repository<ServerVariable> ServerVariablesRepository;
private readonly ServerRepository ServerRepository; private readonly ServerRepository ServerRepository;
private readonly UserRepository UserRepository; private readonly UserRepository UserRepository;
private readonly ImageRepository ImageRepository; private readonly ImageRepository ImageRepository;
@@ -50,7 +52,8 @@ public class ServerService
NodeService nodeService, NodeService nodeService,
NodeAllocationRepository nodeAllocationRepository, NodeAllocationRepository nodeAllocationRepository,
DateTimeService dateTimeService, DateTimeService dateTimeService,
EventSystem eventSystem) EventSystem eventSystem,
Repository<ServerVariable> serverVariablesRepository)
{ {
ServerRepository = serverRepository; ServerRepository = serverRepository;
WingsApiHelper = wingsApiHelper; WingsApiHelper = wingsApiHelper;
@@ -67,6 +70,7 @@ public class ServerService
NodeAllocationRepository = nodeAllocationRepository; NodeAllocationRepository = nodeAllocationRepository;
DateTimeService = dateTimeService; DateTimeService = dateTimeService;
Event = eventSystem; Event = eventSystem;
ServerVariablesRepository = serverVariablesRepository;
} }
private Server EnsureNodeData(Server s) private Server EnsureNodeData(Server s)
@@ -366,6 +370,9 @@ public class ServerService
await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/reinstall", null); await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/reinstall", null);
server.Installing = true;
ServerRepository.Update(server);
await AuditLogService.Log(AuditLogType.ReinstallServer, x => { x.Add<Server>(server.Uuid); }); await AuditLogService.Log(AuditLogType.ReinstallServer, x => { x.Add<Server>(server.Uuid); });
} }
@@ -401,17 +408,15 @@ public class ServerService
public async Task Delete(Server s) public async Task Delete(Server s)
{ {
throw new DisplayException("Deleting a server is currently a bit buggy. So its disabled for your safety"); throw new DisplayException("Deleting servers is currently disabled");
var server = EnsureNodeData(s); var backups = await GetBackups(s);
var backups = await GetBackups(server);
foreach (var backup in backups) foreach (var backup in backups)
{ {
try try
{ {
await DeleteBackup(server, backup); await DeleteBackup(s, backup);
} }
catch (Exception) catch (Exception)
{ {
@@ -419,17 +424,25 @@ 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); 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.Allocations = new();
server.MainAllocation = null; server.MainAllocation = null;
server.Variables = new(); server.Variables = new();
server.Backups = new(); server.Backups = new();
ServerRepository.Update(server); ServerRepository.Update(server);
ServerRepository.Delete(server); ServerRepository.Delete(server);
} }
@@ -439,4 +452,65 @@ public class ServerService
return await NodeService.IsHostUp(server.Node); return await NodeService.IsHostUp(server.Node);
} }
public async Task ArchiveServer(Server server)
{
if (server.IsArchived)
throw new DisplayException("Unable to archive an already archived server");
// Archive server
var backup = await CreateBackup(server);
server.IsArchived = true;
server.Archive = backup;
ServerRepository.Update(server);
await Event.WaitForEvent<ServerBackup>("wings.backups.create", this, x => backup.Id == x.Id);
// Reset server
var access = await CreateFileAccess(server, null!);
var files = await access.Ls();
foreach (var file in files)
{
try
{
await access.Delete(file);
}
catch (Exception)
{
// ignored
}
}
await Event.Emit($"server.{server.Uuid}.archiveStatusChanged", server);
}
public async Task UnArchiveServer(Server s)
{
if (!s.IsArchived)
throw new DisplayException("Unable to unarchive a server which is not archived");
var server = ServerRepository
.Get()
.Include(x => x.Archive)
.First(x => x.Id == s.Id);
if (server.Archive == null)
throw new DisplayException("Archive from server not found");
if (!server.Archive.Created)
throw new DisplayException("Creating the server archive is in progress");
await RestoreBackup(server, server.Archive);
await Event.WaitForEvent<ServerBackup>("wings.backups.restore", this,
x => x.Id == server.Archive.Id);
server.IsArchived = false;
ServerRepository.Update(server);
await Event.Emit($"server.{server.Uuid}.archiveStatusChanged", 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

@@ -159,8 +159,17 @@ public class IdentityService
try try
{ {
var userAgent = HttpContextAccessor.HttpContext.Request.Headers.UserAgent.ToString();
if (userAgent.Contains("Moonlight.App"))
{
var version = userAgent.Remove(0, "Moonlight.App/".Length).Split(' ').FirstOrDefault();
return "Moonlight App " + version;
}
var uaParser = Parser.GetDefault(); var uaParser = Parser.GetDefault();
var info = uaParser.Parse(HttpContextAccessor.HttpContext.Request.Headers.UserAgent); var info = uaParser.Parse(userAgent);
return $"{info.OS} - {info.Device}"; return $"{info.OS} - {info.Device}";
} }

View File

@@ -9,6 +9,7 @@ namespace Moonlight.App.Services.Sessions;
public class SessionService public class SessionService
{ {
private readonly SessionRepository SessionRepository; private readonly SessionRepository SessionRepository;
private Repository<User> UserRepository;
private readonly IdentityService IdentityService; private readonly IdentityService IdentityService;
private readonly NavigationManager NavigationManager; private readonly NavigationManager NavigationManager;
private readonly AlertService AlertService; private readonly AlertService AlertService;
@@ -21,13 +22,15 @@ public class SessionService
IdentityService identityService, IdentityService identityService,
NavigationManager navigationManager, NavigationManager navigationManager,
AlertService alertService, AlertService alertService,
DateTimeService dateTimeService) DateTimeService dateTimeService,
Repository<User> userRepository)
{ {
SessionRepository = sessionRepository; SessionRepository = sessionRepository;
IdentityService = identityService; IdentityService = identityService;
NavigationManager = navigationManager; NavigationManager = navigationManager;
AlertService = alertService; AlertService = alertService;
DateTimeService = dateTimeService; DateTimeService = dateTimeService;
UserRepository = userRepository;
} }
public async Task Register() public async Task Register()
@@ -46,6 +49,12 @@ public class SessionService
}; };
SessionRepository.Add(OwnSession); 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() public void Refresh()

View File

@@ -9,21 +9,36 @@ public class SmartDeployService
private readonly Repository<CloudPanel> CloudPanelRepository; private readonly Repository<CloudPanel> CloudPanelRepository;
private readonly WebSpaceService WebSpaceService; private readonly WebSpaceService WebSpaceService;
private readonly NodeService NodeService; private readonly NodeService NodeService;
private readonly ConfigService ConfigService;
public SmartDeployService( public SmartDeployService(
NodeRepository nodeRepository, NodeRepository nodeRepository,
NodeService nodeService, NodeService nodeService,
WebSpaceService webSpaceService, WebSpaceService webSpaceService,
Repository<CloudPanel> cloudPanelRepository) Repository<CloudPanel> cloudPanelRepository,
ConfigService configService)
{ {
NodeRepository = nodeRepository; NodeRepository = nodeRepository;
NodeService = nodeService; NodeService = nodeService;
WebSpaceService = webSpaceService; WebSpaceService = webSpaceService;
CloudPanelRepository = cloudPanelRepository; CloudPanelRepository = cloudPanelRepository;
ConfigService = configService;
} }
public async Task<Node?> GetNode() 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>(); var data = new Dictionary<Node, double>();
foreach (var node in NodeRepository.Get().ToArray()) foreach (var node in NodeRepository.Get().ToArray())
@@ -59,17 +74,17 @@ public class SmartDeployService
try try
{ {
var cpuStats = await NodeService.GetCpuStats(node); var cpuMetrics = await NodeService.GetCpuMetrics(node);
var memoryStats = await NodeService.GetMemoryStats(node); var memoryMetrics = await NodeService.GetMemoryMetrics(node);
var diskStats = await NodeService.GetDiskStats(node); var diskMetrics = await NodeService.GetDiskMetrics(node);
var cpuWeight = 0.5; // Weight of CPU usage in the final score 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 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 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 cpuScore = (1 - cpuMetrics.CpuUsage) * 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 memoryScore = (1 - (memoryMetrics.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 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; var finalScore = cpuScore + memoryScore + diskSpaceScore;

View File

@@ -7,20 +7,33 @@ namespace Moonlight.App.Services.Statistics;
public class StatisticsViewService public class StatisticsViewService
{ {
private readonly StatisticsRepository StatisticsRepository; private readonly StatisticsRepository StatisticsRepository;
private readonly Repository<User> UserRepository;
private readonly DateTimeService DateTimeService; private readonly DateTimeService DateTimeService;
public StatisticsViewService(StatisticsRepository statisticsRepository, DateTimeService dateTimeService) public StatisticsViewService(StatisticsRepository statisticsRepository, DateTimeService dateTimeService, Repository<User> userRepository)
{ {
StatisticsRepository = statisticsRepository; StatisticsRepository = statisticsRepository;
DateTimeService = dateTimeService; DateTimeService = dateTimeService;
UserRepository = userRepository;
} }
public StatisticsData[] GetData(string chart, StatisticsTimeSpan timeSpan) public StatisticsData[] GetData(string chart, StatisticsTimeSpan timeSpan)
{ {
var startDate = DateTimeService.GetCurrent() - TimeSpan.FromHours((int)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(); 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

@@ -1,4 +1,7 @@
using Microsoft.EntityFrameworkCore; using System.Net;
using DnsClient;
using Logging.Net;
using Microsoft.EntityFrameworkCore;
using Moonlight.App.ApiClients.CloudPanel; using Moonlight.App.ApiClients.CloudPanel;
using Moonlight.App.ApiClients.CloudPanel.Requests; using Moonlight.App.ApiClients.CloudPanel.Requests;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
@@ -18,7 +21,8 @@ public class WebSpaceService
private readonly CloudPanelApiHelper CloudPanelApiHelper; private readonly CloudPanelApiHelper CloudPanelApiHelper;
public WebSpaceService(Repository<CloudPanel> cloudPanelRepository, Repository<WebSpace> webSpaceRepository, CloudPanelApiHelper cloudPanelApiHelper, Repository<MySqlDatabase> databaseRepository) public WebSpaceService(Repository<CloudPanel> cloudPanelRepository, Repository<WebSpace> webSpaceRepository,
CloudPanelApiHelper cloudPanelApiHelper, Repository<MySqlDatabase> databaseRepository)
{ {
CloudPanelRepository = cloudPanelRepository; CloudPanelRepository = cloudPanelRepository;
WebSpaceRepository = webSpaceRepository; WebSpaceRepository = webSpaceRepository;
@@ -37,7 +41,7 @@ public class WebSpaceService
var ftpPassword = StringHelper.GenerateString(16); var ftpPassword = StringHelper.GenerateString(16);
var phpVersion = "8.1"; // TODO: Add config option or smth var phpVersion = "8.1"; // TODO: Add config option or smth
var w = new WebSpace() var w = new WebSpace()
{ {
CloudPanel = cloudPanel, CloudPanel = cloudPanel,
@@ -72,11 +76,21 @@ public class WebSpaceService
public async Task Delete(WebSpace w) public async Task Delete(WebSpace w)
{ {
var website = EnsureData(w); var webSpace = WebSpaceRepository
.Get()
await CloudPanelApiHelper.Delete(website.CloudPanel, $"site/{website.Domain}", null); .Include(x => x.Databases)
.Include(x => x.CloudPanel)
WebSpaceRepository.Delete(website); .Include(x => x.Owner)
.First(x => x.Id == w.Id);
foreach (var database in webSpace.Databases.ToArray())
{
await DeleteDatabase(webSpace, database);
}
await CloudPanelApiHelper.Delete(webSpace.CloudPanel, $"site/{webSpace.Domain}", null);
WebSpaceRepository.Delete(webSpace);
} }
public async Task<bool> IsHostUp(CloudPanel cloudPanel) public async Task<bool> IsHostUp(CloudPanel cloudPanel)
@@ -99,7 +113,7 @@ public class WebSpaceService
return false; return false;
} }
public async Task<bool> IsHostUp(WebSpace w) public async Task<bool> IsHostUp(WebSpace w)
{ {
var webSpace = EnsureData(w); var webSpace = EnsureData(w);
@@ -111,6 +125,52 @@ public class WebSpaceService
{ {
var webspace = EnsureData(w); var webspace = EnsureData(w);
var dns = new LookupClient(new LookupClientOptions()
{
CacheFailedResults = false,
UseCache = false
});
var ipOfWebspaceQuery = await dns.QueryAsync(
webspace.Domain,
QueryType.A
);
var ipOfWebspaceWwwQuery = await dns.QueryAsync(
"www." + webspace.Domain,
QueryType.CNAME
);
var ipOfWebspace = ipOfWebspaceQuery.Answers.ARecords().FirstOrDefault();
var ipOfWebspaceWww = ipOfWebspaceWwwQuery.Answers.CnameRecords().FirstOrDefault();
if (ipOfWebspace == null)
throw new DisplayException($"Unable to find any a records for {webspace.Domain}", true);
if (ipOfWebspaceWww == null)
throw new DisplayException($"Unable to find any cname records for www.{webspace.Domain}", true);
var ipOfHostQuery = await dns.QueryAsync(
webspace.CloudPanel.Host,
QueryType.A
);
var ipOfHost = ipOfHostQuery.Answers.ARecords().FirstOrDefault();
if (ipOfHost == null)
throw new DisplayException("Unable to find a record of host system");
if (ipOfHost.Address.ToString() != ipOfWebspace.Address.ToString())
throw new DisplayException("The dns records of your webspace do not point to the host system");
Logger.Debug($"{ipOfWebspaceWww.CanonicalName.Value}");
if (ipOfWebspaceWww.CanonicalName.Value != webspace.CloudPanel.Host + ".")
throw new DisplayException(
$"The dns record www.{webspace.Domain} does not point to {webspace.CloudPanel.Host}", true);
await CloudPanelApiHelper.Post(webspace.CloudPanel, "letsencrypt/install/certificate", new InstallLetsEncrypt() await CloudPanelApiHelper.Post(webspace.CloudPanel, "letsencrypt/install/certificate", new InstallLetsEncrypt()
{ {
DomainName = webspace.Domain DomainName = webspace.Domain
@@ -148,7 +208,7 @@ public class WebSpaceService
DatabaseUserName = database.UserName, DatabaseUserName = database.UserName,
DatabaseUserPassword = database.Password DatabaseUserPassword = database.Password
}); });
webspace.Databases.Add(database); webspace.Databases.Add(database);
WebSpaceRepository.Update(webspace); WebSpaceRepository.Update(webspace);
} }
@@ -156,7 +216,7 @@ public class WebSpaceService
public async Task DeleteDatabase(WebSpace w, MySqlDatabase database) public async Task DeleteDatabase(WebSpace w, MySqlDatabase database)
{ {
var webspace = EnsureData(w); var webspace = EnsureData(w);
await CloudPanelApiHelper.Delete(webspace.CloudPanel, $"db/{database.UserName}", null); await CloudPanelApiHelper.Delete(webspace.CloudPanel, $"db/{database.UserName}", null);
webspace.Databases.Remove(database); webspace.Databases.Remove(database);
@@ -170,7 +230,8 @@ public class WebSpaceService
var webspace = EnsureData(w); var webspace = EnsureData(w);
return Task.FromResult<FileAccess>( return Task.FromResult<FileAccess>(
new SftpFileAccess(webspace.CloudPanel.Host, webspace.UserName, webspace.Password, 22, true, $"/htdocs/{webspace.Domain}") new SftpFileAccess(webspace.CloudPanel.Host, webspace.UserName, webspace.Password, 22, true,
$"/htdocs/{webspace.Domain}")
); );
} }
@@ -182,7 +243,7 @@ public class WebSpaceService
.Include(x => x.CloudPanel) .Include(x => x.CloudPanel)
.Include(x => x.Owner) .Include(x => x.Owner)
.First(x => x.Id == webSpace.Id); .First(x => x.Id == webSpace.Id);
return webSpace; return webSpace;
} }
} }

View File

@@ -9,6 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="6.0.5" />
<PackageReference Include="Blazor-ApexCharts" Version="0.9.16-beta" /> <PackageReference Include="Blazor-ApexCharts" Version="0.9.16-beta" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Ben.Demystifier" Version="0.4.1" /> <PackageReference Include="Ben.Demystifier" Version="0.4.1" />
@@ -20,9 +21,11 @@
<PackageReference Include="CurrieTechnologies.Razor.SweetAlert2" Version="5.4.0" /> <PackageReference Include="CurrieTechnologies.Razor.SweetAlert2" Version="5.4.0" />
<PackageReference Include="Discord.Net" Version="3.10.0" /> <PackageReference Include="Discord.Net" Version="3.10.0" />
<PackageReference Include="Discord.Net.Webhook" Version="3.10.0" /> <PackageReference Include="Discord.Net.Webhook" Version="3.10.0" />
<PackageReference Include="DnsClient" Version="1.7.0" />
<PackageReference Include="FluentFTP" Version="46.0.2" /> <PackageReference Include="FluentFTP" Version="46.0.2" />
<PackageReference Include="GravatarSharp.Core" Version="1.0.1.2" /> <PackageReference Include="GravatarSharp.Core" Version="1.0.1.2" />
<PackageReference Include="JWT" Version="10.0.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="Logging.Net" Version="1.1.3" />
<PackageReference Include="MailKit" Version="4.0.0" /> <PackageReference Include="MailKit" Version="4.0.0" />
<PackageReference Include="Mappy.Net" Version="1.0.2" /> <PackageReference Include="Mappy.Net" Version="1.0.2" />
@@ -39,13 +42,14 @@
<PackageReference Include="MineStat" Version="3.1.1" /> <PackageReference Include="MineStat" Version="3.1.1" />
<PackageReference Include="MySqlBackup.NET" Version="2.3.8" /> <PackageReference Include="MySqlBackup.NET" Version="2.3.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3-beta1" /> <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="Otp.NET" Version="1.3.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
<PackageReference Include="QRCoder" Version="1.4.3" /> <PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="RestSharp" Version="109.0.0-preview.1" /> <PackageReference Include="RestSharp" Version="109.0.0-preview.1" />
<PackageReference Include="SSH.NET" Version="2020.0.2" /> <PackageReference Include="SSH.NET" Version="2020.0.2" />
<PackageReference Include="UAParser" Version="3.1.47" /> <PackageReference Include="UAParser" Version="3.1.47" />
<PackageReference Include="XtermBlazor" Version="1.6.1" /> <PackageReference Include="XtermBlazor" Version="1.8.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -71,9 +75,27 @@
<ItemGroup> <ItemGroup>
<Folder Include="App\ApiClients\CloudPanel\Resources\" /> <Folder Include="App\ApiClients\CloudPanel\Resources\" />
<Folder Include="App\ApiClients\Daemon\Requests\" />
<Folder Include="App\Http\Middleware" /> <Folder Include="App\Http\Middleware" />
<Folder Include="storage\backups\" /> <Folder Include="storage\backups\" />
<Folder Include="storage\resources\public\background\" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Shared\Views\Server\Settings\DotnetFileSetting.razor" />
<AdditionalFiles Include="Shared\Views\Server\Settings\DotnetVersionSetting.razor" />
<AdditionalFiles Include="Shared\Views\Server\Settings\FabricVersionSetting.razor" />
<AdditionalFiles Include="Shared\Views\Server\Settings\ForgeVersionSetting.razor" />
<AdditionalFiles Include="Shared\Views\Server\Settings\JavaFileSetting.razor" />
<AdditionalFiles Include="Shared\Views\Server\Settings\JavaRuntimeVersionSetting.razor" />
<AdditionalFiles Include="Shared\Views\Server\Settings\JavascriptFileSetting.razor" />
<AdditionalFiles Include="Shared\Views\Server\Settings\JavascriptVersionSetting.razor" />
<AdditionalFiles Include="Shared\Views\Server\Settings\Join2StartSetting.razor" />
<AdditionalFiles Include="Shared\Views\Server\Settings\PaperVersionSetting.razor" />
<AdditionalFiles Include="Shared\Views\Server\Settings\PythonFileSetting.razor" />
<AdditionalFiles Include="Shared\Views\Server\Settings\PythonVersionSetting.razor" />
<AdditionalFiles Include="Shared\Views\Server\Settings\ServerDeleteSetting.razor" />
<AdditionalFiles Include="Shared\Views\Server\Settings\ServerRenameSetting.razor" />
<AdditionalFiles Include="Shared\Views\Server\Settings\ServerResetSetting.razor" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -7,7 +7,6 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@inject ConfigService ConfigService @inject ConfigService ConfigService
@inject BundleService BundleService
@inject LoadingMessageRepository LoadingMessageRepository @inject LoadingMessageRepository LoadingMessageRepository
@{ @{
@@ -38,29 +37,20 @@
<link rel="shortcut icon" href="@(moonlightConfig.GetValue<string>("AppUrl"))/api/moonlight/resources/images/logo.svg"/> <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" href="https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700"/>
<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/boxicons.min.css"/>
<link rel="stylesheet" type="text/css" href="/assets/css/blazor.css"/>
@if (BundleService.BundledFinished)
{ <link rel="stylesheet" type="text/css" href="/_content/XtermBlazor/XtermBlazor.css"/>
<link rel="stylesheet" type="text/css" href="/api/moonlight/resources/bundle/css?idontwannabecached=@(BundleService.CacheId)"/> <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"/>
else
{ <link href="/assets/plugins/global/plugins.bundle.css" rel="stylesheet" type="text/css"/>
foreach (var cssFile in BundleService.CssFiles)
{
if (cssFile.StartsWith("http"))
{
<link rel="stylesheet" type="text/css" href="@(cssFile)">
}
else
{
<style>
@cssFile
</style>
}
}
}
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1"/>
<base href="~/"/> <base href="~/"/>
@@ -106,24 +96,31 @@
</div> </div>
</div> </div>
@if (BundleService.BundledFinished) <script src="/_framework/blazor.server.js"></script>
{ <script src="/assets/plugins/global/plugins.bundle.js"></script>
<script src="/api/moonlight/resources/bundle/js?idontwannabecached=@(BundleService.CacheId)"> <script src="/_content/XtermBlazor/XtermBlazor.min.js"></script>
</script> <script src="/_content/BlazorTable/BlazorTable.min.js"></script>
} <script src="/_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js"></script>
else <script src="/_content/Blazor.ContextMenu/blazorContextMenu.min.js"></script>
{
foreach (var jsFile in BundleService.JsFiles) <script src="https://www.google.com/recaptcha/api.js"></script>
{
if (jsFile.StartsWith("http")) <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.7.0/lib/xterm-addon-fit.min.js"></script>
{
<script src="@(jsFile)"></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>
else <script src="/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js"></script>
{ <script src="/_content/BlazorMonaco/jsInterop.js"></script>
@Html.Raw("<script>" + jsFile +"</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> </body>
</html> </html>

View File

@@ -1,12 +1,15 @@
using BlazorDownloadFile; using BlazorDownloadFile;
using BlazorTable; using BlazorTable;
using CurrieTechnologies.Razor.SweetAlert2; using CurrieTechnologies.Razor.SweetAlert2;
using HealthChecks.UI.Client;
using Logging.Net; using Logging.Net;
using Moonlight.App.ApiClients.CloudPanel; using Moonlight.App.ApiClients.CloudPanel;
using Moonlight.App.ApiClients.Daemon; using Moonlight.App.ApiClients.Daemon;
using Moonlight.App.ApiClients.Modrinth;
using Moonlight.App.ApiClients.Paper; using Moonlight.App.ApiClients.Paper;
using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings;
using Moonlight.App.Database; using Moonlight.App.Database;
using Moonlight.App.Diagnostics.HealthChecks;
using Moonlight.App.Events; using Moonlight.App.Events;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Helpers.Wings; using Moonlight.App.Helpers.Wings;
@@ -16,6 +19,7 @@ using Moonlight.App.Repositories.Domains;
using Moonlight.App.Repositories.LogEntries; using Moonlight.App.Repositories.LogEntries;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;
using Moonlight.App.Services; using Moonlight.App.Services;
using Moonlight.App.Services.Addon;
using Moonlight.App.Services.Background; using Moonlight.App.Services.Background;
using Moonlight.App.Services.DiscordBot; using Moonlight.App.Services.DiscordBot;
using Moonlight.App.Services.Files; using Moonlight.App.Services.Files;
@@ -32,9 +36,6 @@ namespace Moonlight
{ {
public class Program 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) public static async Task Main(string[] args)
{ {
Logger.UsedLogger = new CacheLogger(); Logger.UsedLogger = new CacheLogger();
@@ -66,6 +67,10 @@ namespace Moonlight
options.HandshakeTimeout = TimeSpan.FromSeconds(10); options.HandshakeTimeout = TimeSpan.FromSeconds(10);
}); });
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
builder.Services.AddHealthChecks()
.AddCheck<DatabaseHealthCheck>("Database")
.AddCheck<NodeHealthCheck>("Nodes")
.AddCheck<DaemonHealthCheck>("Daemons");
// Databases // Databases
builder.Services.AddDbContext<DataContext>(); builder.Services.AddDbContext<DataContext>();
@@ -113,7 +118,6 @@ namespace Moonlight
builder.Services.AddScoped<OneTimeJwtService>(); builder.Services.AddScoped<OneTimeJwtService>();
builder.Services.AddSingleton<NotificationServerService>(); builder.Services.AddSingleton<NotificationServerService>();
builder.Services.AddScoped<NotificationAdminService>(); builder.Services.AddScoped<NotificationAdminService>();
builder.Services.AddScoped<NotificationClientService>();
builder.Services.AddScoped<ModalService>(); builder.Services.AddScoped<ModalService>();
builder.Services.AddScoped<SmartDeployService>(); builder.Services.AddScoped<SmartDeployService>();
builder.Services.AddScoped<WebSpaceService>(); builder.Services.AddScoped<WebSpaceService>();
@@ -128,7 +132,8 @@ namespace Moonlight
builder.Services.AddScoped<ReCaptchaService>(); builder.Services.AddScoped<ReCaptchaService>();
builder.Services.AddScoped<IpBanService>(); builder.Services.AddScoped<IpBanService>();
builder.Services.AddSingleton<OAuth2Service>(); builder.Services.AddSingleton<OAuth2Service>();
builder.Services.AddSingleton<BundleService>(); builder.Services.AddScoped<DynamicBackgroundService>();
builder.Services.AddScoped<ServerAddonPluginService>();
builder.Services.AddScoped<SubscriptionService>(); builder.Services.AddScoped<SubscriptionService>();
builder.Services.AddScoped<SubscriptionAdminService>(); builder.Services.AddScoped<SubscriptionAdminService>();
@@ -156,12 +161,16 @@ namespace Moonlight
builder.Services.AddSingleton<HostSystemHelper>(); builder.Services.AddSingleton<HostSystemHelper>();
builder.Services.AddScoped<DaemonApiHelper>(); builder.Services.AddScoped<DaemonApiHelper>();
builder.Services.AddScoped<CloudPanelApiHelper>(); builder.Services.AddScoped<CloudPanelApiHelper>();
builder.Services.AddScoped<ModrinthApiHelper>();
// Background services // Background services
builder.Services.AddSingleton<DiscordBotService>(); builder.Services.AddSingleton<DiscordBotService>();
builder.Services.AddSingleton<StatisticsCaptureService>(); builder.Services.AddSingleton<StatisticsCaptureService>();
builder.Services.AddSingleton<DiscordNotificationService>(); builder.Services.AddSingleton<DiscordNotificationService>();
builder.Services.AddSingleton<CleanupService>(); builder.Services.AddSingleton<CleanupService>();
// Other
builder.Services.AddSingleton<MoonlightService>();
// Third party services // Third party services
builder.Services.AddBlazorTable(); builder.Services.AddBlazorTable();
@@ -187,12 +196,18 @@ namespace Moonlight
app.MapBlazorHub(); app.MapBlazorHub();
app.MapFallbackToPage("/_Host"); app.MapFallbackToPage("/_Host");
app.MapHealthChecks("/_health", new()
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
// AutoStart services // AutoStart services
_ = app.Services.GetRequiredService<CleanupService>(); _ = app.Services.GetRequiredService<CleanupService>();
_ = app.Services.GetRequiredService<DiscordBotService>(); _ = app.Services.GetRequiredService<DiscordBotService>();
_ = app.Services.GetRequiredService<StatisticsCaptureService>(); _ = app.Services.GetRequiredService<StatisticsCaptureService>();
_ = app.Services.GetRequiredService<DiscordNotificationService>(); _ = app.Services.GetRequiredService<DiscordNotificationService>();
_ = app.Services.GetRequiredService<MoonlightService>();
// Discord bot service // Discord bot service
//var discordBotService = app.Services.GetRequiredService<DiscordBotService>(); //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": { "profiles": {
"Moonlight": { "Moonlight": {
"commandName": "Project", "commandName": "Project",
"launchBrowser": true, "launchBrowser": true,
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development",
"ML_DEBUG": "true"
}, },
"applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118", "applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
"dotnetRunMessages": true "dotnetRunMessages": true
@@ -22,7 +15,8 @@
"launchBrowser": true, "launchBrowser": true,
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development", "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", "applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
"dotnetRunMessages": true "dotnetRunMessages": true
@@ -32,17 +26,11 @@
"launchBrowser": false, "launchBrowser": false,
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development", "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", "applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
"dotnetRunMessages": true "dotnetRunMessages": true
},
"Docker": {
"commandName": "Docker",
"launchBrowser": false,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"publishAllPorts": true,
"useSSL": true
} }
} }
} }

View File

@@ -3,10 +3,13 @@
@using Moonlight.App.Services @using Moonlight.App.Services
@using Logging.Net @using Logging.Net
@using Moonlight.App.ApiClients.CloudPanel @using Moonlight.App.ApiClients.CloudPanel
@using Moonlight.App.ApiClients.Daemon
@using Moonlight.App.ApiClients.Modrinth
@using Moonlight.App.ApiClients.Wings @using Moonlight.App.ApiClients.Wings
@inherits ErrorBoundaryBase @inherits ErrorBoundaryBase
@inject AlertService AlertService @inject AlertService AlertService
@inject ConfigService ConfigService
@inject SmartTranslateService SmartTranslateService @inject SmartTranslateService SmartTranslateService
@if (Crashed) @if (Crashed)
@@ -37,14 +40,25 @@ else
protected override async Task OnErrorAsync(Exception exception) protected override async Task OnErrorAsync(Exception exception)
{ {
Logger.Warn(exception); if (ConfigService.DebugMode)
{
Logger.Warn(exception);
}
if (exception is DisplayException displayException) if (exception is DisplayException displayException)
{ {
await AlertService.Error( if (displayException.DoNotTranslate)
SmartTranslateService.Translate("Error"), {
SmartTranslateService.Translate(displayException.Message) await AlertService.Error(
); displayException.Message
);
}
else
{
await AlertService.Error(
SmartTranslateService.Translate(displayException.Message)
);
}
} }
else if (exception is CloudflareException cloudflareException) else if (exception is CloudflareException cloudflareException)
{ {
@@ -56,7 +70,7 @@ else
else if (exception is WingsException wingsException) else if (exception is WingsException wingsException)
{ {
await AlertService.Error( await AlertService.Error(
SmartTranslateService.Translate("Error from daemon"), SmartTranslateService.Translate("Error from wings"),
wingsException.Message wingsException.Message
); );
@@ -64,6 +78,22 @@ else
Logger.Warn($"Wings exception status code: {wingsException.StatusCode}"); Logger.Warn($"Wings exception status code: {wingsException.StatusCode}");
} }
else if (exception is DaemonException daemonException)
{
await AlertService.Error(
SmartTranslateService.Translate("Error from daemon"),
daemonException.Message
);
Logger.Warn($"Wings exception status code: {daemonException.StatusCode}");
}
else if (exception is ModrinthException modrinthException)
{
await AlertService.Error(
SmartTranslateService.Translate("Error from modrinth"),
modrinthException.Message
);
}
else if (exception is CloudPanelException cloudPanelException) else if (exception is CloudPanelException cloudPanelException)
{ {
await AlertService.Error( await AlertService.Error(
@@ -77,6 +107,7 @@ else
} }
else else
{ {
Logger.Warn(exception);
Crashed = true; Crashed = true;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }

View File

@@ -185,6 +185,8 @@ else
await ToastService.Info(SmartTranslateService.Translate("Starting download")); 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

@@ -0,0 +1,51 @@
@using Moonlight.App.Database.Entities
<div class="card mb-5 mb-xl-10">
<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">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/admin/servers/view/@(Server.Id)">
<TL>Overview</TL>
</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="/admin/servers/view/@(Server.Id)/image">
<TL>Image</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="/admin/servers/view/@(Server.Id)/resources">
<TL>Resources</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 3 ? "active" : "")" href="/admin/servers/view/@(Server.Id)/allocations">
<TL>Allocations</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 4 ? "active" : "")" href="/admin/servers/view/@(Server.Id)/archive">
<TL>Archive</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 5 ? "active" : "")" href="/admin/servers/view/@(Server.Id)/debug">
<TL>Debug</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 6 ? "active" : "")" href="/admin/servers/view/@(Server.Id)/delete">
<TL>Delete</TL>
</a>
</li>
</ul>
</div>
</div>
@code
{
[Parameter]
public int Index { get; set; }
[Parameter]
public Server Server { get; set; }
}

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

@@ -0,0 +1,20 @@
@{
var route = "/" + (Router.Route ?? "");
}
@if (route == Path)
{
@ChildContent
}
@code
{
[CascadingParameter]
public SmartRouter Router { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public string Path { get; set; }
}

View File

@@ -0,0 +1,12 @@
<CascadingValue TValue="SmartRouter" Value="@this">
@ChildContent
</CascadingValue>
@code
{
[Parameter]
public string? Route { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
}

View File

@@ -1,10 +0,0 @@
<div class="alert alert-primary d-flex rounded p-6">
<div class="d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap">
<div class="mb-3 mb-md-0 fw-semibold">
<h4 class="text-gray-900 fw-bold"><TL>Addons</TL></h4>
<div class="fs-6 text-gray-700 pe-7">
<TL>This feature is currently not available</TL>
</div>
</div>
</div>
</div>

View File

@@ -10,7 +10,12 @@
<div class="col-xl-4 mb-xl-10"> <div class="col-xl-4 mb-xl-10">
<div class="card h-md-100"> <div class="card h-md-100">
<div class="card-body d-flex flex-column flex-center"> <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> </div>
</div> </div>

View File

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

View File

@@ -5,7 +5,7 @@
<Xterm <Xterm
@ref="Xterm" @ref="Xterm"
Options="TerminalOptions" Options="TerminalOptions"
AddonIds="@(new[] { "xterm-addon-fit", "xterm-addon-search", "xterm-addon-web-links" })" AddonIds="@(new[] { "xterm-addon-fit" })"
OnFirstRender="OnFirstRender"> OnFirstRender="OnFirstRender">
</Xterm> </Xterm>
@@ -48,6 +48,18 @@
{ {
await Xterm.InvokeAddonFunctionVoidAsync("xterm-addon-fit", "fit"); await Xterm.InvokeAddonFunctionVoidAsync("xterm-addon-fit", "fit");
RunOnFirstRender.Invoke(); RunOnFirstRender.Invoke();
await Task.Run(async () =>
{
try
{
await Task.Delay(1000);
await Xterm.InvokeAddonFunctionVoidAsync("xterm-addon-fit", "fit");
await Task.Delay(1000);
await Xterm.InvokeAddonFunctionVoidAsync("xterm-addon-fit", "fit");
}
catch (Exception){}
});
} }
catch (Exception) catch (Exception)
{ {

View File

@@ -20,26 +20,27 @@
@inject ToastService ToastService @inject ToastService ToastService
@inject SmartTranslateService SmartTranslateService @inject SmartTranslateService SmartTranslateService
@inject IpBanService IpBanService @inject IpBanService IpBanService
@inject DynamicBackgroundService DynamicBackgroundService
<GlobalErrorBoundary> @{
@{ var uri = new Uri(NavigationManager.Uri);
var uri = new Uri(NavigationManager.Uri); var pathParts = uri.LocalPath.Split("/").Reverse();
var pathParts = uri.LocalPath.Split("/").Reverse();
var title = ""; var title = "";
foreach (var pathPart in pathParts) foreach (var pathPart in pathParts)
{
if (!string.IsNullOrEmpty(pathPart))
{ {
if (!string.IsNullOrEmpty(pathPart)) if (pathPart == pathParts.Last())
{ title += $"{pathPart.FirstCharToUpper()} ";
if (pathPart == pathParts.Last()) else
title += $"{pathPart.FirstCharToUpper()} "; title += $"{pathPart.FirstCharToUpper()} - ";
else
title += $"{pathPart.FirstCharToUpper()} - ";
}
} }
} }
}
<GlobalErrorBoundary>
<CascadingValue Value="User"> <CascadingValue Value="User">
<PageTitle>@(string.IsNullOrEmpty(title) ? "Dashboard - " : title)Moonlight</PageTitle> <PageTitle>@(string.IsNullOrEmpty(title) ? "Dashboard - " : title)Moonlight</PageTitle>
@@ -56,7 +57,7 @@
<Sidebar></Sidebar> <Sidebar></Sidebar>
<div class="app-main flex-column flex-row-fluid" id="kt_app_main"> <div class="app-main flex-column flex-row-fluid" id="kt_app_main">
<div class="d-flex flex-column flex-column-fluid"> <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 id="kt_app_content_container" class="app-container container-fluid">
<div class="mt-10"> <div class="mt-10">
<SoftErrorBoundary> <SoftErrorBoundary>
@@ -189,11 +190,13 @@
{ {
try try
{ {
DynamicBackgroundService.OnBackgroundImageChanged += async (_, _) => { await InvokeAsync(StateHasChanged); };
IsIpBanned = await IpBanService.IsBanned(); IsIpBanned = await IpBanService.IsBanned();
if(IsIpBanned) if (IsIpBanned)
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
await Event.On<Object>("ipBan.update", this, async _ => await Event.On<Object>("ipBan.update", this, async _ =>
{ {
IsIpBanned = await IpBanService.IsBanned(); IsIpBanned = await IpBanService.IsBanned();
@@ -211,7 +214,13 @@
await SessionService.Register(); 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) if (User != null)
{ {

View File

@@ -3,11 +3,16 @@
@using Moonlight.App.Repositories @using Moonlight.App.Repositories
@using Moonlight.App.Repositories.Domains @using Moonlight.App.Repositories.Domains
@using Moonlight.App.Database.Entities @using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Misc
@using Moonlight.App.Services
@using Newtonsoft.Json
@using Logging.Net
@inject ServerRepository ServerRepository @inject ServerRepository ServerRepository
@inject UserRepository UserRepository @inject UserRepository UserRepository
@inject Repository<WebSpace> WebSpaceRepository @inject Repository<WebSpace> WebSpaceRepository
@inject DomainRepository DomainRepository @inject DomainRepository DomainRepository
@inject ConfigService ConfigService
<OnlyAdmin> <OnlyAdmin>
<LazyLoader Load="Load"> <LazyLoader Load="Load">
@@ -97,6 +102,28 @@
</a> </a>
</div> </div>
</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> </LazyLoader>
</OnlyAdmin> </OnlyAdmin>
@@ -107,6 +134,8 @@
private int DomainCount = 0; private int DomainCount = 0;
private int WebSpaceCount = 0; private int WebSpaceCount = 0;
private HealthCheck? HealthCheckData;
private Task Load(LazyLoader lazyLoader) private Task Load(LazyLoader lazyLoader)
{ {
ServerCount = ServerRepository.Get().Count(); ServerCount = ServerRepository.Get().Count();
@@ -116,4 +145,26 @@
return Task.CompletedTask; 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>
<div class="m-0"> <div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1"> <span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (CpuStats == null) @if (CpuMetrics == null)
{ {
<span class="text-muted"> <span class="text-muted">
<TL>Loading</TL> <TL>Loading</TL>
@@ -50,12 +50,12 @@ else
else else
{ {
<span> <span>
@(CpuStats.Usage)% <TL>of</TL> @(CpuStats.Cores) <TL>Cores used</TL> @(CpuMetrics.CpuUsage)% <TL>of CPU used</TL>
</span> </span>
} }
</span> </span>
<span class="fw-semibold fs-6"> <span class="fw-semibold fs-6">
@if (CpuStats == null) @if (CpuMetrics == null)
{ {
<span class="text-muted"> <span class="text-muted">
<TL>Loading</TL> <TL>Loading</TL>
@@ -63,7 +63,7 @@ else
} }
else else
{ {
<span>@(CpuStats.Model)</span> <span>@(CpuMetrics.CpuModel)</span>
} }
</span> </span>
</div> </div>
@@ -78,7 +78,7 @@ else
</div> </div>
<div class="m-0"> <div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1"> <span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (MemoryStats == null) @if (MemoryMetrics == null)
{ {
<span class="text-muted"> <span class="text-muted">
<TL>Loading</TL> <TL>Loading</TL>
@@ -87,32 +87,12 @@ else
else else
{ {
<span> <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> </span>
<span class="fw-semibold fs-6"> <span class="fw-semibold fs-6">
@if (MemoryStats == null) @*IDK what to put here*@
{
<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>
}
}
</span> </span>
</div> </div>
</div> </div>
@@ -126,7 +106,7 @@ else
</div> </div>
<div class="m-0"> <div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1"> <span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (DiskStats == null) @if (DiskMetrics == null)
{ {
<span class="text-muted"> <span class="text-muted">
<TL>Loading</TL> <TL>Loading</TL>
@@ -135,21 +115,12 @@ else
else else
{ {
<span> <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> </span>
<span class="fw-semibold fs-6"> <span class="fw-semibold fs-6">
@if (DiskStats == null) @*IDK what to put here*@
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
<span>@(DiskStats.Name) - @(DiskStats.DriveFormat)</span>
}
</span> </span>
</div> </div>
</div> </div>
@@ -239,7 +210,7 @@ else
</div> </div>
<div class="m-0"> <div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1"> <span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (ContainerStats == null) @if (DockerMetrics == null)
{ {
<span class="text-muted"> <span class="text-muted">
<TL>Loading</TL> <TL>Loading</TL>
@@ -248,12 +219,12 @@ else
else else
{ {
<span> <span>
<TL>@(ContainerStats.Containers.Count)</TL> <TL>@(DockerMetrics.Containers.Length)</TL>
</span> </span>
} }
</span> </span>
<span class="fw-semibold fs-6"> <span class="fw-semibold fs-6">
@if (ContainerStats == null) @if (DockerMetrics == null)
{ {
<span class="text-muted"> <span class="text-muted">
<TL>Loading</TL> <TL>Loading</TL>
@@ -290,11 +261,11 @@ else
private Node? Node; private Node? Node;
private CpuStats CpuStats; private CpuMetrics CpuMetrics;
private MemoryStats MemoryStats; private MemoryMetrics MemoryMetrics;
private DiskStats DiskStats; private DiskMetrics DiskMetrics;
private DockerMetrics DockerMetrics;
private SystemStatus SystemStatus; private SystemStatus SystemStatus;
private ContainerStats ContainerStats;
private async Task Load(LazyLoader arg) private async Task Load(LazyLoader arg)
{ {
@@ -311,16 +282,16 @@ else
SystemStatus = await NodeService.GetStatus(Node); SystemStatus = await NodeService.GetStatus(Node);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
CpuStats = await NodeService.GetCpuStats(Node); CpuMetrics = await NodeService.GetCpuMetrics(Node);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
MemoryStats = await NodeService.GetMemoryStats(Node); MemoryMetrics = await NodeService.GetMemoryMetrics(Node);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
DiskStats = await NodeService.GetDiskStats(Node); DiskMetrics = await NodeService.GetDiskMetrics(Node);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
ContainerStats = await NodeService.GetContainerStats(Node); DockerMetrics = await NodeService.GetDockerMetrics(Node);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
catch (Exception e) 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

@@ -0,0 +1,74 @@
@page "/admin/notifications/debugging"
@using Moonlight.App.Services.Notifications
@using Moonlight.App.Models.Misc
@using Moonlight.App.Events
@using BlazorTable
@using Moonlight.App.Database.Entities.Notification
@using Moonlight.App.Services
@inject NotificationServerService NotificationServerService
@inject SmartTranslateService SmartTranslateService
@inject EventSystem Event
@implements IDisposable
<OnlyAdmin>
<LazyLoader Load="Load">
<div class="card card-body">
<Table TableItem="ActiveNotificationClient" Items="Clients" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="ActiveNotificationClient" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Client.Id)" Sortable="false" Filterable="true"/>
<Column TableItem="ActiveNotificationClient" Title="@(SmartTranslateService.Translate("User"))" Field="@(x => x.Client.User.Email)" Sortable="false" Filterable="true"/>
<Column TableItem="ActiveNotificationClient" Title="" Field="@(x => x.Client.Id)" Sortable="false" Filterable="false">
<Template>
<WButton Text="@(SmartTranslateService.Translate("Send notification"))"
WorkingText="@(SmartTranslateService.Translate("Working"))"
CssClasses="btn-primary"
OnClick="() => SendSampleNotification(context)">
</WButton>
</Template>
</Column>
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
</Table>
</div>
</LazyLoader>
</OnlyAdmin>
@code
{
private ActiveNotificationClient[] Clients;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await Event.On<NotificationClient>("notifications.addClient", this, async client =>
{
Clients = await NotificationServerService.GetActiveClients();
await InvokeAsync(StateHasChanged);
});
await Event.On<NotificationClient>("notifications.removeClient", this, async client =>
{
Clients = await NotificationServerService.GetActiveClients();
await InvokeAsync(StateHasChanged);
});
}
}
private async Task Load(LazyLoader loader)
{
Clients = await NotificationServerService.GetActiveClients();
}
private async Task SendSampleNotification(ActiveNotificationClient client)
{
await client.SendAction(@"{""action"": ""notify"",""notification"":{""id"":999,""channel"":""Sample Channel"",""content"":""This is a sample Notification"",""title"":""Sample Notification"",""url"":""server/9b724fe2-d882-49c9-8c34-3414c7e4a17e""}}");
}
public async void Dispose()
{
await Event.Off("notifications.addClient", this);
await Event.Off("notifications.removeClient", this);
}
}

View File

@@ -1,4 +1,4 @@
@page "/admin/servers/edit/{id:int}" @page "/admin/servers/editx/{id:int}"
@using Moonlight.App.Services @using Moonlight.App.Services
@using Moonlight.App.Repositories.Servers @using Moonlight.App.Repositories.Servers

View File

@@ -13,240 +13,250 @@
@inject FileDownloadService FileDownloadService @inject FileDownloadService FileDownloadService
<OnlyAdmin> <OnlyAdmin>
<div class="row"> <div class="row">
<LazyLoader @ref="LazyLoader" Load="Load"> <LazyLoader @ref="LazyLoader" Load="Load">
@if (Image == null) @if (Image == null)
{ {
<div class="alert alert-danger"> <div class="alert alert-danger">
<TL>No image with this id found</TL> <TL>No image with this id found</TL>
</div>
}
else
{
<div class="row">
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="card card-body">
<div class="mb-10">
<label class="form-label">
<TL>Name</TL>
</label>
<input @bind="Image.Name" type="text" class="form-control">
</div>
<div class="mb-10">
<label class="form-label">
<TL>Description</TL>
</label>
<textarea @bind="Image.Description" type="text" class="form-control"></textarea>
</div>
</div>
</div>
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="card card-body">
<label class="form-label">
<TL>Tags</TL>
</label>
<div class="input-group mb-5">
<input @bind="AddTagName" type="text" class="form-control" placeholder="@(SmartTranslateService.Translate("Enter tag name"))">
<button @onclick="AddTag" class="btn btn-primary">
<TL>Add</TL>
</button>
</div>
<div>
@if (Tags.Any())
{
<div class="row">
@foreach (var tag in Tags)
{
<button @onclick="() => RemoveTag(tag)" class="col m-3 btn btn-outline-primary mw-25">
@(tag)
</button>
}
</div>
}
else
{
<div class="alert alert-primary">
<TL>No tags found</TL>
</div>
}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="card card-body">
<label class="form-label">
<TL>Docker images</TL>
</label>
<div class="input-group mb-5">
<input @bind="NewDockerImage.Name" type="text" class="form-control" placeholder="@(SmartTranslateService.Translate("Enter docker image name"))">
<button @onclick="AddDockerImage" class="btn btn-primary">
<TL>Add</TL>
</button>
</div>
<div>
@if (Image.DockerImages.Any())
{
<div class="row">
@foreach (var imageDocker in Image.DockerImages)
{
<button @onclick="() => RemoveDockerImage(imageDocker)" class="col m-3 btn btn-outline-primary mw-25">
@(imageDocker.Name)
</button>
}
</div>
}
else
{
<div class="alert alert-primary">
<TL>No docker images found</TL>
</div>
}
</div>
</div>
</div>
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="card card-body">
<div class="mb-10">
<label class="form-label">
<TL>Default image</TL>
</label>
<select @bind="DefaultImageIndex" class="form-select">
@foreach (var image in Image.DockerImages)
{
<option value="@(image.Id)">@(image.Name)</option>
}
</select>
</div>
<div class="mb-10">
<label class="form-label">
<TL>Allocations</TL>
</label>
<input @bind="Image.Allocations" type="number" class="form-control">
</div>
</div>
</div>
</div>
<div class="row mx-0">
<div class="card card-body">
<div class="mb-10">
<label class="form-label">
<TL>Startup command</TL>
</label>
<input @bind="Image.Startup" type="text" class="form-control">
</div>
<div class="row">
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="mb-10">
<label class="form-label">
<TL>Install container</TL>
</label>
<input @bind="Image.InstallDockerImage" type="text" class="form-control">
</div>
</div>
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="mb-10">
<label class="form-label">
<TL>Install entry</TL>
</label>
<input @bind="Image.InstallEntrypoint" type="text" class="form-control">
</div>
</div>
</div>
<div class="card card-flush">
<FileEditor @ref="Editor" Language="shell" InitialData="@(Image.InstallScript)" HideControls="true"/>
</div>
</div>
</div>
<div class="row my-8">
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="card card-body">
<div class="mb-10">
<label class="form-label">
<TL>Configuration files</TL>
</label>
<textarea @bind="Image.ConfigFiles" class="form-control"></textarea>
</div>
</div>
</div>
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="card card-body">
<div class="mb-10">
<label class="form-label">
<TL>Startup detection</TL>
</label>
<input @bind="Image.StartupDetection" type="text" class="form-control">
</div>
<div class="mb-10">
<label class="form-label">
<TL>Stop command</TL>
</label>
<input @bind="Image.StopCommand" type="text" class="form-control">
</div>
</div>
</div>
</div>
<div class="row my-6">
<div class="card card-body">
<div class="input-group mb-5">
<input type="text" @bind="ImageVariable.Key" placeholder="@(SmartTranslateService.Translate("Key"))" class="form-control">
<input type="text" @bind="ImageVariable.DefaultValue" placeholder="@(SmartTranslateService.Translate("Default value"))" class="form-control">
<button @onclick="AddVariable" class="btn btn-primary">
<TL>Add</TL>
</button>
</div>
<div>
@if (Image!.Variables.Any())
{
<div class="row">
@foreach (var variable in Image!.Variables)
{
<div class="input-group mb-3">
<input type="text" @bind="variable.Key" placeholder="@(SmartTranslateService.Translate("Key"))" class="form-control">
<input type="text" @bind="variable.DefaultValue" placeholder="@(SmartTranslateService.Translate("Default value"))" class="form-control">
<button @onclick="() => RemoveVariable(variable)" class="btn btn-danger">
<TL>Remove</TL>
</button>
</div>
}
</div>
}
else
{
<div class="alert alert-primary">
<TL>No variables found</TL>
</div>
}
</div>
</div>
</div>
<div class="row">
<div class="card card-body">
<div class="d-flex justify-content-end">
<a href="/admin/servers/images" class="btn btn-danger me-3">
<TL>Cancel</TL>
</a>
<WButton Text="@(SmartTranslateService.Translate("Export"))"
WorkingText="@(SmartTranslateService.Translate("Exporting"))"
CssClasses="btn-primary me-3"
OnClick="Export">
</WButton>
<WButton Text="@(SmartTranslateService.Translate("Save"))"
WorkingText="@(SmartTranslateService.Translate("Saving"))"
CssClasses="btn-success"
OnClick="Save">
</WButton>
</div>
</div>
</div>
}
</LazyLoader>
</div> </div>
}
else
{
<div class="row">
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="card card-body">
<div class="mb-10">
<label class="form-label">
<TL>Name</TL>
</label>
<input @bind="Image.Name" type="text" class="form-control">
</div>
<div class="mb-10">
<label class="form-label">
<TL>Description</TL>
</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">
<div class="card card-body">
<label class="form-label">
<TL>Tags</TL>
</label>
<div class="input-group mb-5">
<input @bind="AddTagName" type="text" class="form-control" placeholder="@(SmartTranslateService.Translate("Enter tag name"))">
<button @onclick="AddTag" class="btn btn-primary">
<TL>Add</TL>
</button>
</div>
<div>
@if (Tags.Any())
{
<div class="row">
@foreach (var tag in Tags)
{
<button @onclick="() => RemoveTag(tag)" class="col m-3 btn btn-outline-primary mw-25">
@(tag)
</button>
}
</div>
}
else
{
<div class="alert alert-primary">
<TL>No tags found</TL>
</div>
}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="card card-body">
<label class="form-label">
<TL>Docker images</TL>
</label>
<div class="input-group mb-5">
<input @bind="NewDockerImage.Name" type="text" class="form-control" placeholder="@(SmartTranslateService.Translate("Enter docker image name"))">
<button @onclick="AddDockerImage" class="btn btn-primary">
<TL>Add</TL>
</button>
</div>
<div>
@if (Image.DockerImages.Any())
{
<div class="row">
@foreach (var imageDocker in Image.DockerImages)
{
<button @onclick="() => RemoveDockerImage(imageDocker)" class="col m-3 btn btn-outline-primary mw-25">
@(imageDocker.Name)
</button>
}
</div>
}
else
{
<div class="alert alert-primary">
<TL>No docker images found</TL>
</div>
}
</div>
</div>
</div>
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="card card-body">
<div class="mb-10">
<label class="form-label">
<TL>Default image</TL>
</label>
<select @bind="DefaultImageIndex" class="form-select">
@foreach (var image in Image.DockerImages)
{
<option value="@(image.Id)">@(image.Name)</option>
}
</select>
</div>
<div class="mb-10">
<label class="form-label">
<TL>Allocations</TL>
</label>
<input @bind="Image.Allocations" type="number" class="form-control">
</div>
</div>
</div>
</div>
<div class="row mx-0">
<div class="card card-body">
<div class="mb-10">
<label class="form-label">
<TL>Startup command</TL>
</label>
<input @bind="Image.Startup" type="text" class="form-control">
</div>
<div class="row">
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="mb-10">
<label class="form-label">
<TL>Install container</TL>
</label>
<input @bind="Image.InstallDockerImage" type="text" class="form-control">
</div>
</div>
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="mb-10">
<label class="form-label">
<TL>Install entry</TL>
</label>
<input @bind="Image.InstallEntrypoint" type="text" class="form-control">
</div>
</div>
</div>
<div class="card card-flush">
<FileEditor @ref="Editor" Language="shell" InitialData="@(Image.InstallScript)" HideControls="true"/>
</div>
</div>
</div>
<div class="row my-8">
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="card card-body">
<div class="mb-10">
<label class="form-label">
<TL>Configuration files</TL>
</label>
<textarea @bind="Image.ConfigFiles" class="form-control"></textarea>
</div>
</div>
</div>
<div class="col-xl-6 mb-5 mb-xl-10">
<div class="card card-body">
<div class="mb-10">
<label class="form-label">
<TL>Startup detection</TL>
</label>
<input @bind="Image.StartupDetection" type="text" class="form-control">
</div>
<div class="mb-10">
<label class="form-label">
<TL>Stop command</TL>
</label>
<input @bind="Image.StopCommand" type="text" class="form-control">
</div>
</div>
</div>
</div>
<div class="row my-6">
<div class="card card-body">
<div class="input-group mb-5">
<input type="text" @bind="ImageVariable.Key" placeholder="@(SmartTranslateService.Translate("Key"))" class="form-control">
<input type="text" @bind="ImageVariable.DefaultValue" placeholder="@(SmartTranslateService.Translate("Default value"))" class="form-control">
<button @onclick="AddVariable" class="btn btn-primary">
<TL>Add</TL>
</button>
</div>
<div>
@if (Image!.Variables.Any())
{
<div class="row">
@foreach (var variable in Image!.Variables)
{
<div class="input-group mb-3">
<input type="text" @bind="variable.Key" placeholder="@(SmartTranslateService.Translate("Key"))" class="form-control">
<input type="text" @bind="variable.DefaultValue" placeholder="@(SmartTranslateService.Translate("Default value"))" class="form-control">
<button @onclick="() => RemoveVariable(variable)" class="btn btn-danger">
<TL>Remove</TL>
</button>
</div>
}
</div>
}
else
{
<div class="alert alert-primary">
<TL>No variables found</TL>
</div>
}
</div>
</div>
</div>
<div class="row">
<div class="card card-body">
<div class="d-flex justify-content-end">
<a href="/admin/servers/images" class="btn btn-danger me-3">
<TL>Cancel</TL>
</a>
<WButton Text="@(SmartTranslateService.Translate("Export"))"
WorkingText="@(SmartTranslateService.Translate("Exporting"))"
CssClasses="btn-primary me-3"
OnClick="Export">
</WButton>
<WButton Text="@(SmartTranslateService.Translate("Save"))"
WorkingText="@(SmartTranslateService.Translate("Saving"))"
CssClasses="btn-success"
OnClick="Save">
</WButton>
</div>
</div>
</div>
}
</LazyLoader>
</div>
</OnlyAdmin> </OnlyAdmin>
@code @code
@@ -330,7 +340,7 @@
{ {
Image!.DockerImages.Remove(image); Image!.DockerImages.Remove(image);
} }
private void AddVariable() private void AddVariable()
{ {
Image!.Variables.Add(ImageVariable); Image!.Variables.Add(ImageVariable);
@@ -361,7 +371,7 @@
{ {
Image.TagsJson = JsonConvert.SerializeObject(Tags); Image.TagsJson = JsonConvert.SerializeObject(Tags);
Image.InstallScript = await Editor.GetData(); Image.InstallScript = await Editor.GetData();
var json = JsonConvert.SerializeObject(Image, Formatting.Indented); var json = JsonConvert.SerializeObject(Image, Formatting.Indented);
await FileDownloadService.DownloadString(Image.Name + ".json", json); await FileDownloadService.DownloadString(Image.Name + ".json", json);
} }

View File

@@ -44,7 +44,7 @@
</Column> </Column>
<Column TableItem="Server" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false"> <Column TableItem="Server" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template> <Template>
<a href="/admin/servers/edit/@(context.Id)"> <a href="/admin/servers/view/@(context.Id)">
@(SmartTranslateService.Translate("Manage")) @(SmartTranslateService.Translate("Manage"))
</a> </a>
</Template> </Template>

View File

@@ -132,9 +132,9 @@
try 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)) if (Guid.TryParse(container.Name, out Guid uuid))
{ {

View File

@@ -0,0 +1,68 @@
@using Moonlight.App.Database.Entities
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@inject ServerService ServerService
@inject SmartTranslateService SmartTranslateService
@inject AlertService AlertService
<div class="card">
<div class="card-body">
@if (Server.IsArchived)
{
<span class="fs-5 text-warning"><TL>Server is currently archived</TL></span>
}
else
{
<span class="fs-5"><TL>Server is currently not archived</TL></span>
}
</div>
<div class="card-footer">
<div class="text-end">
@if (Server.IsArchived)
{
<WButton Text="@(SmartTranslateService.Translate("Unarchive"))"
WorkingText="@(SmartTranslateService.Translate("Unarchiving"))"
CssClasses="btn-success"
OnClick="UnArchiveServer">
</WButton>
}
else
{
<WButton Text="@(SmartTranslateService.Translate("Archive"))"
WorkingText="@(SmartTranslateService.Translate("Archiving"))"
CssClasses="btn-danger"
OnClick="ArchiveServer">
</WButton>
}
</div>
</div>
</div>
@code
{
[CascadingParameter]
public Server Server { get; set; }
private async Task ArchiveServer()
{
await ServerService.ArchiveServer(Server);
await InvokeAsync(StateHasChanged);
await AlertService.Success(
SmartTranslateService.Translate("Successfully archived the server")
);
}
private async Task UnArchiveServer()
{
await ServerService.UnArchiveServer(Server);
await InvokeAsync(StateHasChanged);
await AlertService.Success(
SmartTranslateService.Translate("Successfully unarchived the server")
);
}
}

View File

@@ -0,0 +1,31 @@
@using Moonlight.App.Database.Entities
@using Moonlight.App.Services
@inject ServerService ServerService
@inject SmartTranslateService SmartTranslateService
<div class="card">
<div class="card-header">
<span class="card-title">
<TL>Reinstall</TL>
</span>
</div>
<div class="card-footer">
<WButton Text="@(SmartTranslateService.Translate("Reinstall"))"
WorkingText="@(SmartTranslateService.Translate("Reinstalling"))"
CssClasses="btn-warning"
OnClick="Reinstall">
</WButton>
</div>
</div>
@code
{
[CascadingParameter]
public Server Server { get; set; }
private async Task Reinstall()
{
await ServerService.Reinstall(Server!);
}
}

View File

@@ -0,0 +1,111 @@
@using Moonlight.App.Database.Entities
@using Moonlight.App.Repositories
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.Models.Forms
@using Mappy.Net
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@inject Repository<Moonlight.App.Database.Entities.Image> ImageRepository
@inject Repository<Server> ServerRepository
@inject SmartTranslateService SmartTranslateService
@inject ToastService ToastService
<LazyLoader Load="Load">
<SmartForm Model="Model" OnValidSubmit="OnValidSubmit">
<div class="card">
<div class="card-body p-10">
<label class="form-label">
<TL>Override startup command</TL>
</label>
<div class="input-group mb-5">
<span class="input-group-text">
<i class="bx bx-terminal"></i>
</span>
<InputText @bind-Value="Model.OverrideStartup" type="text" class="form-control" placeholder="@(Server.Image.Startup)"></InputText>
</div>
<label class="form-label">
<TL>Docker image</TL>
</label>
<select @bind="Model.DockerImageIndex" class="form-select">
@foreach (var image in DockerImages)
{
<option value="@(DockerImages.IndexOf(image))">@(image.Name)</option>
}
</select>
</div>
<div class="card-body">
@foreach (var vars in Server.Variables.Chunk(4))
{
<div class="row mb-3">
@foreach (var variable in vars)
{
<div class="col">
<div class="card card-body">
<label class="form-label">
<TL>Name</TL>
</label>
<div class="input-group mb-5">
<input @bind="variable.Key" type="text" class="form-control disabled" disabled="">
</div>
<label class="form-label">
<TL>Value</TL>
</label>
<div class="input-group mb-5">
<input @bind="variable.Value" type="text" class="form-control">
</div>
</div>
</div>
}
</div>
}
</div>
<div class="card-footer">
<div class="text-end">
<button type="submit" class="btn btn-success">
<TL>Save changes</TL>
</button>
</div>
</div>
</div>
</SmartForm>
</LazyLoader>
@code
{
[CascadingParameter]
public Server Server { get; set; }
private List<DockerImage> DockerImages;
private List<Moonlight.App.Database.Entities.Image> Images;
private ServerImageDataModel Model;
private Task Load(LazyLoader arg)
{
Images = ImageRepository
.Get()
.Include(x => x.Variables)
.Include(x => x.DockerImages)
.ToList();
DockerImages = Images
.First(x => x.Id == Server.Image.Id).DockerImages
.ToList();
Model = Mapper.Map<ServerImageDataModel>(Server);
return Task.CompletedTask;
}
private async Task OnValidSubmit()
{
Server = Mapper.Map(Server, Model);
ServerRepository.Update(Server);
await ToastService.Success(
SmartTranslateService.Translate("Successfully saved changes")
);
}
}

View File

@@ -0,0 +1,79 @@
@page "/admin/servers/view/{Id:int}/{Route?}"
@using Moonlight.App.Repositories
@using Moonlight.App.Database.Entities
@using Microsoft.EntityFrameworkCore
@using Moonlight.Shared.Components.Navigations
@inject Repository<Server> ServerRepository
<OnlyAdmin>
<LazyLoader @ref="LazyLoader" Load="Load">
@if (Server == null)
{
<div class="alert alert-danger">
<TL>No server with this id found</TL>
</div>
}
else
{
<CascadingValue TValue="Server" Value="Server">
<SmartRouter Route="@Route">
<Route Path="/">
<AdminServersViewNavigation Index="0" Server="Server"/>
<Overview/>
</Route>
<Route Path="/image">
<AdminServersViewNavigation Index="1" Server="Server"/>
<Image/>
</Route>
<Route Path="/resources">
<AdminServersViewNavigation Index="2" Server="Server"/>
<Resources/>
</Route>
<Route Path="/allocations">
<AdminServersViewNavigation Index="3" Server="Server"/>
</Route>
<Route Path="/archive">
<AdminServersViewNavigation Index="4" Server="Server"/>
<Archive/>
</Route>
<Route Path="/debug">
<AdminServersViewNavigation Index="5" Server="Server"/>
<Debug/>
</Route>
<Route Path="/delete">
<AdminServersViewNavigation Index="6" Server="Server"/>
</Route>
</SmartRouter>
</CascadingValue>
}
</LazyLoader>
</OnlyAdmin>
@code
{
[Parameter]
public string? Route { get; set; }
[Parameter]
public int Id { get; set; }
private LazyLoader LazyLoader;
private Server? Server;
private Task Load(LazyLoader arg)
{
Server = ServerRepository
.Get()
.Include(x => x.Image)
.Include(x => x.Owner)
.Include(x => x.Archive)
.Include(x => x.Allocations)
.Include(x => x.MainAllocation)
.Include(x => x.Variables)
.FirstOrDefault(x => x.Id == Id);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,91 @@
@using Moonlight.App.Repositories
@using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Forms
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@using Mappy.Net
@inject Repository<User> UserRepository
@inject Repository<Server> ServerRepository
@inject ToastService ToastService
@inject SmartTranslateService SmartTranslateService
<LazyLoader Load="Load">
<SmartForm Model="Model" OnValidSubmit="OnValidSubmit">
<div class="card">
<div class="card-body p-10">
<label class="form-label">
<TL>Identifier</TL>
</label>
<div class="input-group mb-5">
<span class="input-group-text">
<i class="bx bx-id-card"></i>
</span>
<input type="number" class="form-control disabled" disabled="" value="@(Server.Id)">
</div>
<label class="form-label">
<TL>UuidIdentifier</TL>
</label>
<div class="input-group mb-5">
<span class="input-group-text">
<i class="bx bx-id-card"></i>
</span>
<input type="text" class="form-control disabled" disabled="" value="@(Server.Uuid)">
</div>
<label class="form-label">
<TL>Server name</TL>
</label>
<div class="input-group mb-5">
<span class="input-group-text">
<i class="bx bx-purchase-tag-alt"></i>
</span>
<InputText @bind-Value="Model.Name" type="text" class="form-control" placeholder="@(SmartTranslateService.Translate("Server name"))"></InputText>
</div>
<label class="form-label">
<TL>Owner</TL>
</label>
<div class="input-group mb-5">
<SmartDropdown T="User"
@bind-Value="Model.Owner"
Items="Users"
DisplayFunc="@(x => x.Email)"
SearchProp="@(x => x.Email)">
</SmartDropdown>
</div>
</div>
<div class="card-footer">
<div class="text-end">
<button type="submit" class="btn btn-success"><TL>Save changes</TL></button>
</div>
</div>
</div>
</SmartForm>
</LazyLoader>
@code
{
[CascadingParameter]
public Server Server { get; set; }
private ServerOverviewDataModel Model;
private User[] Users;
private Task Load(LazyLoader arg)
{
Users = UserRepository.Get().ToArray();
Model = Mapper.Map<ServerOverviewDataModel>(Server);
return Task.CompletedTask;
}
private async Task OnValidSubmit()
{
Server = Mapper.Map(Server, Model);
ServerRepository.Update(Server);
await ToastService.Success(
SmartTranslateService.Translate("Successfully saved changes")
);
}
}

View File

@@ -0,0 +1,88 @@
@using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Forms
@using Moonlight.App.Repositories
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@using Mappy.Net
@inject Repository<Server> ServerRepository
@inject SmartTranslateService SmartTranslateService
@inject ToastService ToastService
<LazyLoader Load="Load">
<SmartForm Model="Model" OnValidSubmit="OnValidSubmit">
<div class="card">
<div class="card-body p-10">
<label class="form-label">
<TL>Cpu cores</TL>
</label>
<div class="input-group mb-5">
<span class="input-group-text">
<i class="bx bx-chip"></i>
</span>
<InputNumber @bind-Value="Model.Cpu" type="number" class="form-control"></InputNumber>
<span class="input-group-text">
<TL>CPU Cores (100% = 1 Core)</TL>
</span>
</div>
<label class="form-label">
<TL>Memory</TL>
</label>
<div class="input-group mb-5">
<span class="input-group-text">
<i class="bx bx-microchip"></i>
</span>
<InputNumber @bind-Value="Model.Memory" type="number" class="form-control"></InputNumber>
<span class="input-group-text">
MB
</span>
</div>
<label class="form-label">
<TL>Disk</TL>
</label>
<div class="input-group mb-5">
<span class="input-group-text">
<i class="bx bx-hdd"></i>
</span>
<InputNumber @bind-Value="Model.Disk" type="number" class="form-control"></InputNumber>
<span class="input-group-text">
MB
</span>
</div>
</div>
<div class="card-footer">
<div class="text-end">
<button type="submit" class="btn btn-success">
<TL>Save changes</TL>
</button>
</div>
</div>
</div>
</SmartForm>
</LazyLoader>
@code
{
[CascadingParameter]
public Server Server { get; set; }
private ServerResourcesDataModel Model;
private Task Load(LazyLoader arg)
{
Model = Mapper.Map<ServerResourcesDataModel>(Server);
return Task.CompletedTask;
}
private async Task OnValidSubmit()
{
Server = Mapper.Map(Server, Model);
ServerRepository.Update(Server);
await ToastService.Success(
SmartTranslateService.Translate("Successfully saved changes")
);
}
}

View File

@@ -66,6 +66,20 @@
} }
</div> </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> </LazyLoader>
</OnlyAdmin> </OnlyAdmin>
@@ -73,7 +87,9 @@
{ {
private StatisticsTimeSpan StatisticsTimeSpan = StatisticsTimeSpan.Day; private StatisticsTimeSpan StatisticsTimeSpan = StatisticsTimeSpan.Day;
private LazyLoader Loader; private LazyLoader Loader;
private Dictionary<string, StatisticsData[]> Charts = new(); private Dictionary<string, StatisticsData[]> Charts = new();
private int ActiveUsers = 0;
private int TimeSpanBind private int TimeSpanBind
{ {
@@ -91,34 +107,48 @@
Charts.Add( Charts.Add(
SmartTranslateService.Translate("Servers"), SmartTranslateService.Translate("Servers"),
StatisticsViewService.GetData("serversCount", StatisticsTimeSpan) AvgHelper.Calculate(
StatisticsViewService.GetData("serversCount", StatisticsTimeSpan)
)
); );
Charts.Add( Charts.Add(
SmartTranslateService.Translate("Users"), SmartTranslateService.Translate("Users"),
StatisticsViewService.GetData("usersCount", StatisticsTimeSpan) AvgHelper.Calculate(
StatisticsViewService.GetData("usersCount", StatisticsTimeSpan)
)
); );
Charts.Add( Charts.Add(
SmartTranslateService.Translate("Domains"), SmartTranslateService.Translate("Domains"),
StatisticsViewService.GetData("domainsCount", StatisticsTimeSpan) AvgHelper.Calculate(
StatisticsViewService.GetData("domainsCount", StatisticsTimeSpan)
)
); );
Charts.Add( Charts.Add(
SmartTranslateService.Translate("Databases"), SmartTranslateService.Translate("Databases"),
StatisticsViewService.GetData("databasesCount", StatisticsTimeSpan) AvgHelper.Calculate(
StatisticsViewService.GetData("databasesCount", StatisticsTimeSpan)
)
); );
Charts.Add( Charts.Add(
SmartTranslateService.Translate("Webspaces"), SmartTranslateService.Translate("Webspaces"),
StatisticsViewService.GetData("webspacesCount", StatisticsTimeSpan) AvgHelper.Calculate(
StatisticsViewService.GetData("webspacesCount", StatisticsTimeSpan)
)
); );
Charts.Add( Charts.Add(
SmartTranslateService.Translate("Sessions"), SmartTranslateService.Translate("Sessions"),
StatisticsViewService.GetData("sessionsCount", StatisticsTimeSpan) AvgHelper.Calculate(
StatisticsViewService.GetData("sessionsCount", StatisticsTimeSpan)
)
); );
ActiveUsers = StatisticsViewService.GetActiveUsers(StatisticsTimeSpan);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -2,73 +2,102 @@
@using Moonlight.Shared.Components.Navigations @using Moonlight.Shared.Components.Navigations
@using Moonlight.App.Helpers @using Moonlight.App.Helpers
@using Moonlight.App.Services
@inject HostSystemHelper HostSystemHelper @inject HostSystemHelper HostSystemHelper
@inject MoonlightService MoonlightService
<OnlyAdmin> <OnlyAdmin>
<AdminSystemNavigation Index="0"/> <AdminSystemNavigation Index="0"/>
<div class="row"> <LazyLoader Load="Load">
<div class="col-xxl-6 my-3"> <div class="row">
<div class="card"> <div class="col-xxl-6 my-3">
<div class="card-header"> <div class="card">
<span class="card-title"> <div class="card-header">
<TL>Version</TL> <span class="card-title">
</span> <TL>Version</TL>
</span>
</div>
<div class="card-body">
<span class="fs-5">
<TL>You are running moonlight version</TL>
<span class="text-primary">@(MoonlightService.AppVersion)</span>
</span>
</div>
</div>
</div> </div>
<div class="card-body"> <div class="col-xxl-6 my-3">
<span class="fs-5"> <div class="card">
<TL>You are running moonlight version</TL> <div class="card-header">
<span class="text-primary">@(Program.AppVersion)</span> <span class="card-title">
</span> <TL>Operating system</TL>
</span>
</div>
<div class="card-body">
<span class="fs-5">
<TL>Moonlight is running on</TL>
<span class="text-primary">@(HostSystemHelper.GetOsName())</span>
</span>
</div>
</div>
</div>
<div class="col-xxl-6 my-3">
<div class="card">
<div class="card-header">
<span class="card-title">
<TL>Memory usage</TL>
</span>
</div>
<div class="card-body">
<span class="fs-5">
<TL>Moonlight is using</TL>
<span class="text-primary">@(HostSystemHelper.GetMemoryUsage()) MB</span>
<TL>of memory</TL>
</span>
</div>
</div>
</div>
<div class="col-xxl-6 my-3">
<div class="card">
<div class="card-header">
<span class="card-title">
<TL>Cpu usage</TL>
</span>
</div>
<div class="card-body">
<span class="fs-5">
<TL>Moonlight is using</TL>
<span class="text-primary">@(HostSystemHelper.GetCpuUsage()) %</span>
</span>
</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>
</div> </div>
</div> </LazyLoader>
<div class="col-xxl-6 my-3"> </OnlyAdmin>
<div class="card">
<div class="card-header"> @code
<span class="card-title"> {
<TL>Operating system</TL> private Task Load(LazyLoader arg)
</span> {
</div> return Task.CompletedTask;
<div class="card-body"> }
<span class="fs-5"> }
<TL>Moonlight is running on</TL>
<span class="text-primary">@(HostSystemHelper.GetOsName())</span>
</span>
</div>
</div>
</div>
<div class="col-xxl-6 my-3">
<div class="card">
<div class="card-header">
<span class="card-title">
<TL>Memory usage</TL>
</span>
</div>
<div class="card-body">
<span class="fs-5">
<TL>Moonlight is using</TL>
<span class="text-primary">@(HostSystemHelper.GetMemoryUsage()) MB</span>
<TL>of memory</TL>
</span>
</div>
</div>
</div>
<div class="col-xxl-6 my-3">
<div class="card">
<div class="card-header">
<span class="card-title">
<TL>Cpu usage</TL>
</span>
</div>
<div class="card-body">
<span class="fs-5">
<TL>Moonlight is using</TL>
<span class="text-primary">@(HostSystemHelper.GetCpuUsage()) %</span>
</span>
</div>
</div>
</div>
</div>
</OnlyAdmin>

View File

@@ -46,7 +46,7 @@
else else
{ {
<Table TableItem="Session" Items="AllSessions" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted"> <Table TableItem="Session" Items="AllSessions" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="Session" Title="@(SmartTranslateService.Translate("Email"))" Field="@(x => x.User.Id)" Sortable="true" Filterable="true" Width="20%"> <Column TableItem="Session" Title="@(SmartTranslateService.Translate("Email"))" Field="@(x => x.User.Email)" Sortable="true" Filterable="true" Width="20%">
<Template> <Template>
@if (context.User == null) @if (context.User == null)
{ {

View File

@@ -1,275 +1,16 @@
@page "/changelog" @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; int i = 0;
} }
<div class="card card-docs flex-row-fluid mb-2"> <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="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"> <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++; i++;
<div class="accordion-item mb-5"> <div class="accordion-item mb-5">

View File

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

View File

@@ -125,111 +125,115 @@
</div> </div>
</div> </div>
<!--d-flex flex-row mb-5--> <div class="row">
<div class="card mb-5"> <div class="col">
<div class="card-header card-header-stretch"> <div class="card mb-5">
<div class="card-title d-flex align-items-center"> <div class="card-header card-header-stretch">
<h3 class="fw-bold m-0 text-gray-800"> <div class="card-title d-flex align-items-center">
<TL>Create something new</TL> <h3 class="fw-bold m-0 text-gray-800">
</h3> <TL>Create something new</TL>
</div> </h3>
</div>
<div class="card-body pt-3">
<div class="flex-row">
<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="/servers/create" class="text-gray-800 text-hover-primary mb-1 fs-5">
<TL>Create a gameserver</TL>
</a>
<span class="text-gray-400 fw-semibold d-block fs-6">
<TL>A new gameserver in just a few minutes</TL>
</span>
</div> </div>
</div> </div>
<div class="separator mb-2 mt-2"></div> <div class="card-body pt-3">
<div class="d-flex align-items-center"> <div class="flex-row">
<div class="symbol symbol-50px me-3"> <div class="d-flex align-items-center">
<i class="bx bx-md bx-globe"></i> <div class="symbol symbol-50px me-3">
</div> <i class="bx bx-md bx-server"></i>
<div class="d-flex justify-content-start flex-column"> </div>
<a href="/webspaces/create" class="text-gray-800 text-hover-primary mb-1 fs-5"> <div class="d-flex justify-content-start flex-column">
<TL>Create a webspace</TL> <a href="/servers/create" class="text-gray-800 text-hover-primary mb-1 fs-5">
</a> <TL>Create a gameserver</TL>
<span class="text-gray-400 fw-semibold d-block fs-6"> </a>
<TL>Make your own websites with a webspace</TL> <span class="text-gray-400 fw-semibold d-block fs-6">
</span> <TL>A new gameserver in just a few minutes</TL>
</div> </span>
</div> </div>
<div class="separator mb-2 mt-2"></div> </div>
<div class="d-flex align-items-center"> <div class="separator mb-2 mt-2"></div>
<div class="symbol symbol-50px me-3"> <div class="d-flex align-items-center">
<i class="bx bx-md bx-purchase-tag"></i> <div class="symbol symbol-50px me-3">
</div> <i class="bx bx-md bx-globe"></i>
<div class="d-flex justify-content-start flex-column"> </div>
<a href="/domains/create" class="text-gray-800 text-hover-primary mb-1 fs-5"> <div class="d-flex justify-content-start flex-column">
<TL>Create a domain</TL> <a href="/webspaces/create" class="text-gray-800 text-hover-primary mb-1 fs-5">
</a> <TL>Create a webspace</TL>
<span class="text-gray-400 fw-semibold d-block fs-6"> </a>
<TL>Make your servvices accessible throught your own domain</TL> <span class="text-gray-400 fw-semibold d-block fs-6">
</span> <TL>Make your own websites with a webspace</TL>
</span>
</div>
</div>
<div class="separator mb-2 mt-2"></div>
<div class="d-flex align-items-center">
<div class="symbol symbol-50px me-3">
<i class="bx bx-md bx-purchase-tag"></i>
</div>
<div class="d-flex justify-content-start flex-column">
<a href="/domains/create" class="text-gray-800 text-hover-primary mb-1 fs-5">
<TL>Create a domain</TL>
</a>
<span class="text-gray-400 fw-semibold d-block fs-6">
<TL>Make your services accessible through your own domain</TL>
</span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="col">
<div class="card mb-5">
<div class="card mb-5"> <div class="card-header card-header-stretch">
<div class="card-header card-header-stretch"> <div class="card-title d-flex align-items-center">
<div class="card-title d-flex align-items-center"> <h3 class="fw-bold m-0 text-gray-800">
<h3 class="fw-bold m-0 text-gray-800"> <TL>Manage your services</TL>
<TL>Manage your services</TL> </h3>
</h3>
</div>
</div>
<div class="card-body pt-3">
<div class="flex-row">
<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="/servers" class="text-gray-800 text-hover-primary mb-1 fs-5">
<TL>Manage your gameservers</TL>
</a>
<span class="text-gray-400 fw-semibold d-block fs-6">
<TL>Adjust your gameservers</TL>
</span>
</div> </div>
</div> </div>
<div class="separator mb-2 mt-2"></div> <div class="card-body pt-3">
<div class="d-flex align-items-center"> <div class="flex-row">
<div class="symbol symbol-50px me-3"> <div class="d-flex align-items-center">
<i class="bx bx-md bx-globe"></i> <div class="symbol symbol-50px me-3">
</div> <i class="bx bx-md bx-server"></i>
<div class="d-flex justify-content-start flex-column"> </div>
<a href="/webspaces" class="text-gray-800 text-hover-primary mb-1 fs-5"> <div class="d-flex justify-content-start flex-column">
<TL>Manage your webspaces</TL> <a href="/servers" class="text-gray-800 text-hover-primary mb-1 fs-5">
</a> <TL>Manage your gameservers</TL>
<span class="text-gray-400 fw-semibold d-block fs-6"> </a>
<TL>Modify the content of your webspaces</TL> <span class="text-gray-400 fw-semibold d-block fs-6">
</span> <TL>Adjust your gameservers</TL>
</div> </span>
</div> </div>
<div class="separator mb-2 mt-2"></div> </div>
<div class="d-flex align-items-center"> <div class="separator mb-2 mt-2"></div>
<div class="symbol symbol-50px me-3"> <div class="d-flex align-items-center">
<i class="bx bx-md bx-purchase-tag"></i> <div class="symbol symbol-50px me-3">
</div> <i class="bx bx-md bx-globe"></i>
<div class="d-flex justify-content-start flex-column"> </div>
<a href="/domains" class="text-gray-800 text-hover-primary mb-1 fs-5"> <div class="d-flex justify-content-start flex-column">
<TL>Manage your domains</TL> <a href="/webspaces" class="text-gray-800 text-hover-primary mb-1 fs-5">
</a> <TL>Manage your webspaces</TL>
<span class="text-gray-400 fw-semibold d-block fs-6"> </a>
<TL>Add, edit and delete dns records</TL> <span class="text-gray-400 fw-semibold d-block fs-6">
</span> <TL>Modify the content of your webspaces</TL>
</span>
</div>
</div>
<div class="separator mb-2 mt-2"></div>
<div class="d-flex align-items-center">
<div class="symbol symbol-50px me-3">
<i class="bx bx-md bx-purchase-tag"></i>
</div>
<div class="d-flex justify-content-start flex-column">
<a href="/domains" class="text-gray-800 text-hover-primary mb-1 fs-5">
<TL>Manage your domains</TL>
</a>
<span class="text-gray-400 fw-semibold d-block fs-6">
<TL>Add, edit and delete dns records</TL>
</span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,13 +5,12 @@
@using Logging.Net @using Logging.Net
@using Moonlight.App.Database.Entities @using Moonlight.App.Database.Entities
@using Moonlight.App.Events @using Moonlight.App.Events
@using Moonlight.App.Helpers
@using Moonlight.App.Helpers.Wings @using Moonlight.App.Helpers.Wings
@using Moonlight.App.Helpers.Wings.Enums @using Moonlight.App.Helpers.Wings.Enums
@using Moonlight.App.Repositories @using Moonlight.App.Repositories
@using Moonlight.App.Services @using Moonlight.App.Services
@using Moonlight.App.Services.Sessions
@using Moonlight.Shared.Components.Xterm @using Moonlight.Shared.Components.Xterm
@using Moonlight.Shared.Components.ServerControl
@using Newtonsoft.Json @using Newtonsoft.Json
@inject ImageRepository ImageRepository @inject ImageRepository ImageRepository
@@ -20,6 +19,8 @@
@inject EventSystem Event @inject EventSystem Event
@inject ServerService ServerService @inject ServerService ServerService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject DynamicBackgroundService DynamicBackgroundService
@inject SmartTranslateService SmartTranslateService
@implements IDisposable @implements IDisposable
@@ -76,66 +77,65 @@
</div> </div>
</div> </div>
} }
else if (CurrentServer.IsArchived)
{
<div class="d-flex justify-content-center flex-center">
<div class="card">
<img src="/assets/media/svg/archive.svg" class="card-img-top w-50 mx-auto pt-5" alt="Not found image"/>
<div class="card-body text-center">
<h1 class="card-title">
<TL>Server is currently archived</TL>
</h1>
<p class="card-text fs-4">
<TL>This server is archived. The data of this server is stored as a backup. To restore click the unarchive button an be patient</TL>
</p>
<WButton Text="@(SmartTranslateService.Translate("Unarchive"))"
WorkingText="@(SmartTranslateService.Translate("Please wait"))"
CssClasses="btn-primary"
OnClick="UnArchive">
</WButton>
</div>
</div>
</div>
}
else else
{ {
<CascadingValue Value="Console"> <CascadingValue Value="Console">
<CascadingValue Value="CurrentServer"> <CascadingValue Value="CurrentServer">
<CascadingValue Value="Tags"> <CascadingValue Value="Tags">
<CascadingValue Value="Node"> <SmartRouter Route="@Route">
<CascadingValue Value="Image"> <Route Path="/">
<CascadingValue Value="NodeAllocation"> <ServerNavigation Index="0">
@{ <ServerConsole/>
var index = 0; </ServerNavigation>
</Route>
switch (Route) <Route Path="/files">
{ <ServerNavigation Index="1">
case "files": <ServerFiles/>
index = 1; </ServerNavigation>
break; </Route>
case "backups": <Route Path="/backups">
index = 2; <ServerNavigation Index="2">
break; <ServerBackups/>
case "network": </ServerNavigation>
index = 3; </Route>
break; <Route Path="/network">
case "addons": <ServerNavigation Index="3">
index = 4; <ServerNetwork/>
break; </ServerNavigation>
case "settings": </Route>
index = 5; <Route Path="/addons">
break; <ServerNavigation Index="4">
default: <ServerAddons/>
index = 0; </ServerNavigation>
break; </Route>
} <Route Path="/settings">
} <ServerNavigation Index="5">
<ServerSettings/>
<ServerNavigation Index="index"> </ServerNavigation>
@switch (Route) </Route>
{ </SmartRouter>
case "files":
<ServerFiles></ServerFiles>
break;
case "backups":
<ServerBackups></ServerBackups>
break;
case "network":
<ServerNetwork></ServerNetwork>
break;
case "addons":
<ServerAddons></ServerAddons>
break;
case "settings":
<ServerSettings></ServerSettings>
break;
default:
<ServerConsole></ServerConsole>
break;
}
</ServerNavigation>
</CascadingValue>
</CascadingValue>
</CascadingValue>
</CascadingValue> </CascadingValue>
</CascadingValue> </CascadingValue>
</CascadingValue> </CascadingValue>
@@ -234,7 +234,7 @@
} }
catch (Exception) catch (Exception)
{ {
// ignored // ignored
} }
if (CurrentServer != null) if (CurrentServer != null)
@@ -258,8 +258,8 @@
.Include(x => x.Variables) .Include(x => x.Variables)
.First(x => x.Id == CurrentServer.Image.Id); .First(x => x.Id == CurrentServer.Image.Id);
// Live variable migration // Live variable migration
foreach (var variable in image.Variables) foreach (var variable in image.Variables)
{ {
if (!CurrentServer.Variables.Any(x => x.Key == variable.Key)) if (!CurrentServer.Variables.Any(x => x.Key == variable.Key))
@@ -269,13 +269,13 @@
Key = variable.Key, Key = variable.Key,
Value = variable.DefaultValue Value = variable.DefaultValue
}); });
ServerRepository.Update(CurrentServer); ServerRepository.Update(CurrentServer);
} }
} }
// Tags // Tags
await lazyLoader.SetText("Requesting tags"); await lazyLoader.SetText("Requesting tags");
Tags = JsonConvert.DeserializeObject<string[]>(image.TagsJson) ?? Array.Empty<string>(); Tags = JsonConvert.DeserializeObject<string[]>(image.TagsJson) ?? Array.Empty<string>();
@@ -291,6 +291,18 @@
return Task.CompletedTask; return Task.CompletedTask;
}); });
await Event.On<Server>($"server.{CurrentServer.Uuid}.archiveStatusChanged", this, server =>
{
NavigationManager.NavigateTo(NavigationManager.Uri, true);
return Task.CompletedTask;
});
if (string.IsNullOrEmpty(Image.BackgroundImageUrl))
await DynamicBackgroundService.Reset();
else
await DynamicBackgroundService.Change(Image.BackgroundImageUrl);
} }
} }
else else
@@ -298,7 +310,7 @@
Logger.Debug("Server is null"); Logger.Debug("Server is null");
} }
} }
private async Task ReconnectConsole() private async Task ReconnectConsole()
{ {
await Console!.Disconnect(); await Console!.Disconnect();
@@ -310,6 +322,7 @@
if (CurrentServer != null) if (CurrentServer != null)
{ {
await Event.Off($"server.{CurrentServer.Uuid}.installComplete", this); await Event.Off($"server.{CurrentServer.Uuid}.installComplete", this);
await Event.Off($"server.{CurrentServer.Uuid}.archiveStatusChanged", this);
} }
if (Console != null) if (Console != null)
@@ -317,4 +330,9 @@
Console.Dispose(); Console.Dispose();
} }
} }
private async Task UnArchive()
{
await ServerService.UnArchiveServer(CurrentServer!);
}
} }

Some files were not shown because too many files have changed in this diff Show More