Compare commits
14 Commits
e7b1e77d0a
...
v2.1
| Author | SHA1 | Date | |
|---|---|---|---|
| ecf0e01914 | |||
| 6d447a0ff9 | |||
| ba5e364c05 | |||
| 2a2ce28b5f | |||
| 91887ec047 | |||
| 2fc371c219 | |||
| 9470e06c0f | |||
| 2f8665f1d4 | |||
| 4d4f35e2be | |||
| 609ea3a443 | |||
| 3e19b29cde | |||
| 1475b89660 | |||
| 3bb9a08630 | |||
| 252c4103f3 |
69
.gitea/workflows/publish-nuget.yml
Normal file
69
.gitea/workflows/publish-nuget.yml
Normal file
@@ -0,0 +1,69 @@
|
||||
name: "Dev Publish: Nuget"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- v2.1
|
||||
paths:
|
||||
- 'MoonlightServers.*/*.csproj'
|
||||
|
||||
env:
|
||||
NUGET_SOURCE: https://git.battlestati.one/api/packages/Moonlight-Panel/nuget/index.json
|
||||
NUGET_PUBLIC: https://api.nuget.org/v3/index.json
|
||||
CONFIGURATION: Debug
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish ${{ matrix.project }}
|
||||
runs-on: linux_amd64
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
project:
|
||||
- MoonlightServers.Api
|
||||
- MoonlightServers.Shared
|
||||
- MoonlightServers.DaemonShared
|
||||
- MoonlightServers.Frontend
|
||||
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore NuGet packages
|
||||
run: >
|
||||
dotnet restore ${{ matrix.project }}
|
||||
--source ${{ env.NUGET_PUBLIC }}
|
||||
--source ${{ env.NUGET_SOURCE }}
|
||||
|
||||
# Frontend requires a host build + Tailwind compilation first
|
||||
- name: Build frontend host (Frontend only)
|
||||
if: matrix.project == 'MoonlightServers.Frontend'
|
||||
run: >
|
||||
dotnet build Hosts/MoonlightServers.Frontend.Host
|
||||
--configuration ${{ env.CONFIGURATION }}
|
||||
|
||||
- name: Build Tailwind styles (Frontend only)
|
||||
if: matrix.project == 'MoonlightServers.Frontend'
|
||||
working-directory: Hosts/MoonlightServers.Frontend.Host/Styles
|
||||
run: npm install && npm run build
|
||||
|
||||
- name: Build project
|
||||
run: >
|
||||
dotnet build ${{ matrix.project }}
|
||||
--configuration ${{ env.CONFIGURATION }}
|
||||
--no-restore
|
||||
|
||||
- name: Pack NuGet package
|
||||
run: >
|
||||
dotnet pack ${{ matrix.project }}
|
||||
--configuration ${{ env.CONFIGURATION }}
|
||||
--output ./artifacts
|
||||
--no-build
|
||||
|
||||
- name: Push NuGet package
|
||||
run: >
|
||||
dotnet nuget push ./artifacts/*.nupkg
|
||||
--skip-duplicate
|
||||
--source ${{ env.NUGET_SOURCE }}
|
||||
--api-key ${{ secrets.ACCESS_TOKEN }}
|
||||
@@ -8,15 +8,14 @@
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.3"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.5"/>
|
||||
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="SimplePlugin" Version="1.0.2"/>
|
||||
|
||||
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.3"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.3" PrivateAssets="all"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.5" PrivateAssets="all"/>
|
||||
<PackageReference Include="SimplePlugin" Version="1.0.2"/>
|
||||
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -15,8 +15,8 @@ export default function extractTailwindClasses(opts = {}) {
|
||||
},
|
||||
OnceExit() {
|
||||
const classArray = Array.from(classSet).sort();
|
||||
fs.mkdirSync('../../../Servers.Frontend/Styles', { recursive: true });
|
||||
fs.writeFileSync('../../../Servers.Frontend/Styles/Servers.Frontend.map', classArray.join('\n'));
|
||||
fs.mkdirSync('../../../MoonlightServers.Frontend/Styles', { recursive: true });
|
||||
fs.writeFileSync('../../../MoonlightServers.Frontend/Styles/MoonlightServers.Frontend.map', classArray.join('\n'));
|
||||
console.log(`Extracted classes ${classArray.length}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
@import "../bin/ShadcnBlazor/default-theme.css";
|
||||
@import "./theme.css";
|
||||
|
||||
@source "../bin/Moonlight.Frontend/*.map";
|
||||
@source "../bin/**/*.map";
|
||||
|
||||
@source "../../../MoonlightServers.Api/**/*.razor";
|
||||
@source "../../../MoonlightServers.Api/**/*.cs";
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
<script src="/_content/ShadcnBlazor/interop.js" defer></script>
|
||||
<script src="/_content/ShadcnBlazor.Extras/interop.js" defer></script>
|
||||
<script src="/_content/ShadcnBlazor.Extras/codemirror-bundle.js" defer></script>
|
||||
|
||||
<script src="/_content/Moonlight.Frontend/chart.umd.js" defer></script>
|
||||
|
||||
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Moonlight.Shared.Http.Requests;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Shared;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.DaemonShared.Http.Daemon;
|
||||
using MoonlightServers.Shared.Admin.Nodes;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
@@ -14,4 +15,6 @@ public static partial class NodeMapper
|
||||
public static partial IQueryable<NodeDto> ProjectToDto(this IQueryable<Node> nodes);
|
||||
public static partial Node ToEntity(CreateNodeDto dto);
|
||||
public static partial void Merge([MappingTarget] Node node, UpdateNodeDto dto);
|
||||
|
||||
public static partial NodeStatisticsDto ToDto(SystemStatisticsDto dto);
|
||||
}
|
||||
@@ -54,6 +54,19 @@ public class NodeService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SystemStatisticsDto> GetStatisticsAsync(Node node)
|
||||
{
|
||||
var client = ClientFactory.CreateClient();
|
||||
|
||||
var request = CreateBaseRequest(node, HttpMethod.Get, "api/system/statistics");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
await EnsureSuccessAsync(response);
|
||||
|
||||
return (await response.Content.ReadFromJsonAsync<SystemStatisticsDto>(SerializationContext.Default.Options))!;
|
||||
}
|
||||
|
||||
private static HttpRequestMessage CreateBaseRequest(
|
||||
Node node,
|
||||
[StringSyntax(StringSyntaxAttribute.Uri)]
|
||||
|
||||
40
MoonlightServers.Api/Admin/Nodes/StatisticsController.cs
Normal file
40
MoonlightServers.Api/Admin/Nodes/StatisticsController.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.Shared;
|
||||
using MoonlightServers.Shared.Admin.Nodes;
|
||||
|
||||
namespace MoonlightServers.Api.Admin.Nodes;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Policy = Permissions.Nodes.View)]
|
||||
[Route("api/admin/servers/nodes/{id:int}/statistics")]
|
||||
public class StatisticsController : Controller
|
||||
{
|
||||
private readonly NodeService NodeService;
|
||||
private readonly DatabaseRepository<Node> NodeRepository;
|
||||
|
||||
public StatisticsController(NodeService nodeService, DatabaseRepository<Node> nodeRepository)
|
||||
{
|
||||
NodeService = nodeService;
|
||||
NodeRepository = nodeRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<NodeStatisticsDto>> GetAsync([FromRoute] int id)
|
||||
{
|
||||
var node = await NodeRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
||||
if (node == null)
|
||||
return Problem("No node with this id found", statusCode: 404);
|
||||
|
||||
var statistics = await NodeService.GetStatisticsAsync(node);
|
||||
var dto = NodeMapper.ToDto(statistics);
|
||||
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.Shared.Http.Requests;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Shared;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Json;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Shared;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.Shared;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Shared;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.Shared;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moonlight.Api.Configuration;
|
||||
using Moonlight.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database;
|
||||
@@ -28,7 +28,12 @@ public class DataContext : DbContext
|
||||
$"Port={Options.Value.Port};" +
|
||||
$"Username={Options.Value.Username};" +
|
||||
$"Password={Options.Value.Password};" +
|
||||
$"Database={Options.Value.Database}"
|
||||
$"Database={Options.Value.Database}",
|
||||
builder =>
|
||||
{
|
||||
builder.MigrationsAssembly(typeof(DataContext).Assembly);
|
||||
builder.MigrationsHistoryTable("MigrationsHistory", "servers");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ public class DbMigrationService : IHostedLifecycleService
|
||||
await using var scope = ServiceProvider.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||
|
||||
var availableMigrations = context.Database.GetMigrations();
|
||||
Logger.LogTrace("Available migrations: {names}", string.Join(", ", availableMigrations));
|
||||
|
||||
var pendingMigrations = await context.Database.GetPendingMigrationsAsync(cancellationToken);
|
||||
var migrationNames = pendingMigrations.ToArray();
|
||||
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20260312153948_AddedNullabilityForTemplateVariableDefaultValue")]
|
||||
partial class AddedNullabilityForTemplateVariableDefaultValue
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("servers")
|
||||
.HasAnnotation("ProductVersion", "10.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Node", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("HttpEndpointUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Nodes", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("AllowUserDockerImageChange")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Author")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<int?>("DefaultDockerImageId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("DonateUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("UpdateUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "FilesConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "ConfigurationFiles", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile", b2 =>
|
||||
{
|
||||
b2.IsRequired();
|
||||
|
||||
b2.Property<string>("Parser")
|
||||
.IsRequired();
|
||||
|
||||
b2.Property<string>("Path")
|
||||
.IsRequired();
|
||||
|
||||
b2.ComplexCollection(typeof(List<Dictionary<string, object>>), "Mappings", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile.Mappings#ConfigurationFileMapping", b3 =>
|
||||
{
|
||||
b3.IsRequired();
|
||||
|
||||
b3.Property<string>("Key")
|
||||
.IsRequired();
|
||||
|
||||
b3.Property<string>("Value");
|
||||
});
|
||||
});
|
||||
|
||||
b1
|
||||
.ToJson("FilesConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "InstallationConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.InstallationConfig#InstallationConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("DockerImage")
|
||||
.IsRequired();
|
||||
|
||||
b1.Property<string>("Script")
|
||||
.IsRequired();
|
||||
|
||||
b1.Property<string>("Shell")
|
||||
.IsRequired();
|
||||
|
||||
b1
|
||||
.ToJson("InstallationConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "LifecycleConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.PrimitiveCollection<string>("OnlineLogPatterns")
|
||||
.IsRequired();
|
||||
|
||||
b1.Property<string>("StopCommand")
|
||||
.IsRequired();
|
||||
|
||||
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "StartupCommands", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig.StartupCommands#StartupCommand", b2 =>
|
||||
{
|
||||
b2.IsRequired();
|
||||
|
||||
b2.Property<string>("Command")
|
||||
.IsRequired();
|
||||
|
||||
b2.Property<string>("DisplayName")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
b1
|
||||
.ToJson("LifecycleConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "MiscellaneousConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.MiscellaneousConfig#MiscellaneousConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("UseLegacyStartup");
|
||||
|
||||
b1
|
||||
.ToJson("MiscellaneousConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DefaultDockerImageId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Templates", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<bool>("SkipPulling")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("TemplateId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TemplateId");
|
||||
|
||||
b.ToTable("TemplateDockerImages", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DefaultValue")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("EnvName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("character varying(60)");
|
||||
|
||||
b.Property<int>("TemplateId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TemplateId");
|
||||
|
||||
b.ToTable("TemplateVariablesVariables", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||
{
|
||||
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", "DefaultDockerImage")
|
||||
.WithOne()
|
||||
.HasForeignKey("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "DefaultDockerImageId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("DefaultDockerImage");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
|
||||
{
|
||||
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
|
||||
.WithMany("DockerImages")
|
||||
.HasForeignKey("TemplateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Template");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
|
||||
{
|
||||
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
|
||||
.WithMany("Variables")
|
||||
.HasForeignKey("TemplateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Template");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||
{
|
||||
b.Navigation("DockerImages");
|
||||
|
||||
b.Navigation("Variables");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddedNullabilityForTemplateVariableDefaultValue : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "DefaultValue",
|
||||
schema: "servers",
|
||||
table: "TemplateVariablesVariables",
|
||||
type: "character varying(1024)",
|
||||
maxLength: 1024,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(1024)",
|
||||
oldMaxLength: 1024);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "DefaultValue",
|
||||
schema: "servers",
|
||||
table: "TemplateVariablesVariables",
|
||||
type: "character varying(1024)",
|
||||
maxLength: 1024,
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(1024)",
|
||||
oldMaxLength: 1024,
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,8 +126,7 @@ namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||
b3.Property<string>("Key")
|
||||
.IsRequired();
|
||||
|
||||
b3.Property<string>("Value")
|
||||
.IsRequired();
|
||||
b3.Property<string>("Value");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -239,7 +238,6 @@ namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DefaultValue")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
|
||||
@@ -6,14 +6,23 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Nuget Package Settings">
|
||||
<Version>2.1.0</Version>
|
||||
<Title>MoonlightServers.Api</Title>
|
||||
<Authors>Moonlight Panel</Authors>
|
||||
<Description>Development package of MoonlightServers.Api</Description>
|
||||
<Copyright>Moonlight Panel</Copyright>
|
||||
<PackageProjectUrl>https://git.battlestati.one/Moonlight-Panel/Servers</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://git.battlestati.one/Moonlight-Panel/Servers/src/branch/v2.1/LICENSE</PackageLicenseUrl>
|
||||
<RepositoryUrl>https://git.battlestati.one/Moonlight-Panel/Servers</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<SupportedPlatform Include="browser"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.3" />
|
||||
<PackageReference Include="Moonlight.Api" Version="2.1.0">
|
||||
<ExcludeAssets>content;contentfiles</ExcludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -8,8 +8,8 @@ using MoonlightServers.Api.Admin.Templates;
|
||||
using MoonlightServers.Api.Infrastructure.Configuration;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
|
||||
using MoonlightServers.Shared;
|
||||
using SimplePlugin.Abstractions;
|
||||
using SerializationContext = MoonlightServers.Shared.SerializationContext;
|
||||
|
||||
namespace MoonlightServers.Api;
|
||||
|
||||
|
||||
36
MoonlightServers.Daemon/Helpers/NativeMethods.cs
Normal file
36
MoonlightServers.Daemon/Helpers/NativeMethods.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MoonlightServers.Daemon.Helpers;
|
||||
|
||||
internal static partial class NativeMethods
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct StatVfsResult
|
||||
{
|
||||
public ulong bsize;
|
||||
public ulong frsize;
|
||||
public ulong blocks;
|
||||
public ulong bfree;
|
||||
public ulong bavail;
|
||||
public ulong files;
|
||||
public ulong ffree;
|
||||
public ulong favail;
|
||||
public ulong fsid;
|
||||
public ulong flag;
|
||||
public ulong namemax;
|
||||
private ulong __spare0; // } kernel reserved padding —
|
||||
private ulong __spare1; // } never read, exist only to
|
||||
private ulong __spare2; // } match the 112-byte struct
|
||||
private ulong __spare3; // } statvfs layout on x86-64
|
||||
private ulong __spare4; // } Linux so the fields above
|
||||
private ulong __spare5; // } land at the right offsets
|
||||
}
|
||||
|
||||
// SetLastError = true tells the marshaller to capture errno immediately
|
||||
// after the call, before any other code can clobber it. Retrieve it with
|
||||
// Marshal.GetLastPInvokeError() which maps to the thread-local errno value.
|
||||
[LibraryImport("libc", EntryPoint = "statvfs",
|
||||
StringMarshalling = StringMarshalling.Utf8,
|
||||
SetLastError = true)]
|
||||
internal static partial int StatVfs(string path, out StatVfsResult buf);
|
||||
}
|
||||
76
MoonlightServers.Daemon/Helpers/SystemMetrics.Cpu.cs
Normal file
76
MoonlightServers.Daemon/Helpers/SystemMetrics.Cpu.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
namespace MoonlightServers.Daemon.Helpers;
|
||||
|
||||
public partial class SystemMetrics
|
||||
{
|
||||
public record CpuSnapshot(
|
||||
string ModelName,
|
||||
double TotalUsagePercent,
|
||||
IReadOnlyList<double> CoreUsagePercents
|
||||
);
|
||||
|
||||
private record RawCpuLine(
|
||||
long User,
|
||||
long Nice,
|
||||
long System,
|
||||
long Idle,
|
||||
long Iowait,
|
||||
long Irq,
|
||||
long Softirq,
|
||||
long Steal
|
||||
);
|
||||
|
||||
private static async Task<List<RawCpuLine>> ReadRawCpuStatsAsync()
|
||||
{
|
||||
var lines = await File.ReadAllLinesAsync("/proc/stat");
|
||||
var result = new List<RawCpuLine>();
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
// All cpu* lines appear at the top of the file; stop on the first non-cpu line.
|
||||
if (!line.StartsWith("cpu", StringComparison.Ordinal))
|
||||
break;
|
||||
|
||||
var p = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (p.Length < 8)
|
||||
continue;
|
||||
|
||||
result.Add(new RawCpuLine(
|
||||
User: long.Parse(p[1]),
|
||||
Nice: long.Parse(p[2]),
|
||||
System: long.Parse(p[3]),
|
||||
Idle: long.Parse(p[4]),
|
||||
Iowait: long.Parse(p[5]),
|
||||
Irq: long.Parse(p[6]),
|
||||
Softirq: long.Parse(p[7]),
|
||||
Steal: p.Length > 8 ? long.Parse(p[8]) : 0L
|
||||
));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task<string> ReadCpuModelNameAsync()
|
||||
{
|
||||
var lines = await File.ReadAllLinesAsync("/proc/cpuinfo");
|
||||
var line = lines.FirstOrDefault(l => l.StartsWith("model name", StringComparison.OrdinalIgnoreCase));
|
||||
return line is not null ? line.Split(':')[1].Trim() : "Unknown";
|
||||
}
|
||||
|
||||
private static CpuSnapshot ComputeCpuUsage(string modelName, List<RawCpuLine> s1, List<RawCpuLine> s2)
|
||||
{
|
||||
// Index 0 = aggregate "cpu" row; indices 1+ = "cpu0", "cpu1"
|
||||
var totalUsage = s1.Count > 0 ? Usage(s1[0], s2[0]) : 0.0;
|
||||
var coreUsages = s1.Skip(1).Zip(s2.Skip(1), Usage).ToList();
|
||||
|
||||
return new CpuSnapshot(modelName, totalUsage, coreUsages);
|
||||
|
||||
static double Usage(RawCpuLine a, RawCpuLine b)
|
||||
{
|
||||
var idleDelta = (b.Idle + b.Iowait) - (a.Idle + a.Iowait);
|
||||
var totalDelta = (b.User + b.Nice + b.System + b.Idle + b.Iowait + b.Irq + b.Softirq + b.Steal)
|
||||
- (a.User + a.Nice + a.System + a.Idle + a.Iowait + a.Irq + a.Softirq + a.Steal);
|
||||
return totalDelta <= 0 ? 0.0 : Math.Round((1.0 - (double)idleDelta / totalDelta) * 100.0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
149
MoonlightServers.Daemon/Helpers/SystemMetrics.Disk.cs
Normal file
149
MoonlightServers.Daemon/Helpers/SystemMetrics.Disk.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MoonlightServers.Daemon.Helpers;
|
||||
|
||||
public partial class SystemMetrics
|
||||
{
|
||||
public record DiskInfo(
|
||||
string MountPoint,
|
||||
string Device,
|
||||
string FileSystem,
|
||||
long TotalBytes,
|
||||
long UsedBytes,
|
||||
long FreeBytes,
|
||||
double UsedPercent,
|
||||
long InodesTotal,
|
||||
long InodesUsed,
|
||||
long InodesFree,
|
||||
double InodesUsedPercent
|
||||
);
|
||||
|
||||
private static readonly HashSet<string> IgnoredFileSystems = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"overlay", "aufs", // Container image layers
|
||||
"tmpfs", "devtmpfs", "ramfs", // RAM-backed virtual filesystems
|
||||
"sysfs", "proc", "devpts", // Kernel virtual filesystems
|
||||
"cgroup", "cgroup2",
|
||||
"pstore", "securityfs", "debugfs", "tracefs",
|
||||
"mqueue", "hugetlbfs", "fusectl", "configfs",
|
||||
"binfmt_misc", "nsfs", "rpc_pipefs",
|
||||
"squashfs", // Snap package loop mounts
|
||||
};
|
||||
|
||||
private static readonly string[] IgnoredMountPrefixes =
|
||||
{
|
||||
"/sys",
|
||||
"/proc",
|
||||
"/dev",
|
||||
"/run/docker",
|
||||
"/var/lib/docker",
|
||||
"/var/lib/containers",
|
||||
"/boot", "/boot/efi"
|
||||
};
|
||||
|
||||
private static async Task<IReadOnlyList<DiskInfo>> ReadDisksAsync()
|
||||
{
|
||||
var lines = await File.ReadAllLinesAsync("/proc/mounts");
|
||||
var results = new List<DiskInfo>();
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var parts = line.Split(' ');
|
||||
if (parts.Length < 4) continue;
|
||||
|
||||
var device = parts[0];
|
||||
var mountPoint = UnescapeOctal(parts[1]);
|
||||
var fsType = parts[2];
|
||||
|
||||
if (IgnoredFileSystems.Contains(fsType)) continue;
|
||||
if (IgnoredMountPrefixes.Any(p => mountPoint.StartsWith(p, StringComparison.Ordinal))) continue;
|
||||
if (device == "none" || device.StartsWith("//", StringComparison.Ordinal)) continue;
|
||||
|
||||
var disk = ReadSingleDisk(device, mountPoint, fsType);
|
||||
|
||||
if (disk is not null)
|
||||
results.Add(disk);
|
||||
}
|
||||
|
||||
return results
|
||||
.GroupBy(d => d.Device)
|
||||
.Select(g => g.OrderBy(d => d.MountPoint.Length).First())
|
||||
.OrderBy(d => d.MountPoint)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static DiskInfo? ReadSingleDisk(string device, string mountPoint, string fsType)
|
||||
{
|
||||
if (NativeMethods.StatVfs(mountPoint, out var st) != 0)
|
||||
{
|
||||
var errno = Marshal.GetLastPInvokeError();
|
||||
Console.WriteLine($"statvfs({mountPoint}) failed: errno {errno} ({new Win32Exception(errno).Message})");
|
||||
return null;
|
||||
}
|
||||
|
||||
var blockSize = (long)st.frsize;
|
||||
var totalBytes = (long)st.blocks * blockSize;
|
||||
var freeBytes = (long)st.bavail * blockSize;
|
||||
|
||||
if (totalBytes <= 0) return null;
|
||||
|
||||
var usedBytes = totalBytes - ((long)st.bfree * blockSize);
|
||||
var usedPct = Math.Round((double)usedBytes / totalBytes * 100.0, 2);
|
||||
|
||||
long inodesTotal, inodesUsed, inodesFree;
|
||||
double inodePct;
|
||||
|
||||
if (st.files == 0)
|
||||
{
|
||||
// Filesystem doesn't expose inode counts (FAT, exFAT, NTFS via ntfs-3g, etc.)
|
||||
inodesTotal = inodesUsed = inodesFree = -1;
|
||||
inodePct = -1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
inodesTotal = (long)st.files;
|
||||
inodesFree = (long)st.ffree;
|
||||
inodesUsed = inodesTotal - inodesFree;
|
||||
inodePct = Math.Round((double)inodesUsed / inodesTotal * 100.0, 2);
|
||||
}
|
||||
|
||||
return new DiskInfo(
|
||||
MountPoint: mountPoint,
|
||||
Device: device,
|
||||
FileSystem: fsType,
|
||||
TotalBytes: totalBytes,
|
||||
UsedBytes: usedBytes,
|
||||
FreeBytes: freeBytes,
|
||||
UsedPercent: usedPct,
|
||||
InodesTotal: inodesTotal,
|
||||
InodesUsed: inodesUsed,
|
||||
InodesFree: inodesFree,
|
||||
InodesUsedPercent: inodePct
|
||||
);
|
||||
}
|
||||
|
||||
private static string UnescapeOctal(string s)
|
||||
{
|
||||
if (!s.Contains('\\')) return s;
|
||||
|
||||
var sb = new System.Text.StringBuilder(s.Length);
|
||||
for (var i = 0; i < s.Length; i++)
|
||||
{
|
||||
if (s[i] == '\\' && i + 3 < s.Length
|
||||
&& s[i + 1] is >= '0' and <= '7'
|
||||
&& s[i + 2] is >= '0' and <= '7'
|
||||
&& s[i + 3] is >= '0' and <= '7')
|
||||
{
|
||||
sb.Append((char)((s[i + 1] - '0') * 64 + (s[i + 2] - '0') * 8 + (s[i + 3] - '0')));
|
||||
i += 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(s[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
59
MoonlightServers.Daemon/Helpers/SystemMetrics.Memory.cs
Normal file
59
MoonlightServers.Daemon/Helpers/SystemMetrics.Memory.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
namespace MoonlightServers.Daemon.Helpers;
|
||||
|
||||
public partial class SystemMetrics
|
||||
{
|
||||
/// <summary>Memory figures derived from <c>/proc/meminfo</c>.</summary>
|
||||
/// <param name="TotalBytes">Physical RAM installed.</param>
|
||||
/// <param name="UsedBytes">RAM actively in use (<c>Total - Available</c>).</param>
|
||||
/// <param name="FreeBytes">Completely unallocated RAM.</param>
|
||||
/// <param name="CachedBytes">Page cache + reclaimable slab — matches the <c>cached</c> column in <c>free -h</c>.</param>
|
||||
/// <param name="BuffersBytes">Kernel I/O buffer memory.</param>
|
||||
/// <param name="AvailableBytes">Estimated RAM available for new allocations without swapping.</param>
|
||||
/// <param name="UsedPercent">UsedBytes / TotalBytes as a percentage (0–100).</param>
|
||||
public record MemoryInfo(
|
||||
long TotalBytes,
|
||||
long UsedBytes,
|
||||
long FreeBytes,
|
||||
long CachedBytes,
|
||||
long BuffersBytes,
|
||||
long AvailableBytes,
|
||||
double UsedPercent
|
||||
);
|
||||
|
||||
// Memory — /proc/meminfo
|
||||
|
||||
/// <summary>
|
||||
/// Parses <c>/proc/meminfo</c> into a <see cref="MemoryInfo"/> record.
|
||||
/// The <c>CachedBytes</c> field is computed as
|
||||
/// <c>Cached + SReclaimable - Shmem</c> to match the value shown by <c>free -h</c>.
|
||||
/// </summary>
|
||||
private static async Task<MemoryInfo> ReadMemoryAsync()
|
||||
{
|
||||
var lines = await File.ReadAllLinesAsync("/proc/meminfo");
|
||||
var map = new Dictionary<string, long>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var colon = line.IndexOf(':');
|
||||
if (colon < 0)
|
||||
continue;
|
||||
|
||||
var key = line[..colon].Trim();
|
||||
var val = line[(colon + 1)..].Trim().Split(' ')[0]; // Strip "kB" suffix.
|
||||
if (long.TryParse(val, out var kb))
|
||||
map[key] = kb * 1024L;
|
||||
}
|
||||
|
||||
var total = map.GetValueOrDefault("MemTotal");
|
||||
var available = map.GetValueOrDefault("MemAvailable");
|
||||
var free = map.GetValueOrDefault("MemFree");
|
||||
var buffers = map.GetValueOrDefault("Buffers");
|
||||
var cached = map.GetValueOrDefault("Cached")
|
||||
+ map.GetValueOrDefault("SReclaimable")
|
||||
- map.GetValueOrDefault("Shmem");
|
||||
var used = total - available;
|
||||
var usedPct = total > 0 ? Math.Round((double)used / total * 100.0, 2) : 0.0;
|
||||
|
||||
return new MemoryInfo(total, used, free, cached, buffers, available, usedPct);
|
||||
}
|
||||
}
|
||||
101
MoonlightServers.Daemon/Helpers/SystemMetrics.Network.cs
Normal file
101
MoonlightServers.Daemon/Helpers/SystemMetrics.Network.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
namespace MoonlightServers.Daemon.Helpers;
|
||||
|
||||
public partial class SystemMetrics
|
||||
{
|
||||
/// <summary>Network throughput for a single interface, computed between two samples.</summary>
|
||||
/// <param name="Name">Interface name, e.g. <c>eth0</c>, <c>ens3</c>.</param>
|
||||
/// <param name="RxBytesPerSec">Received bytes per second.</param>
|
||||
/// <param name="TxBytesPerSec">Transmitted bytes per second.</param>
|
||||
/// <param name="RxPacketsPerSec">Received packets per second.</param>
|
||||
/// <param name="TxPacketsPerSec">Transmitted packets per second.</param>
|
||||
/// <param name="RxErrors">Cumulative receive error count (not a rate).</param>
|
||||
/// <param name="TxErrors">Cumulative transmit error count (not a rate).</param>
|
||||
public record NetworkInterfaceInfo(
|
||||
string Name,
|
||||
long RxBytesPerSec,
|
||||
long TxBytesPerSec,
|
||||
long RxPacketsPerSec,
|
||||
long TxPacketsPerSec,
|
||||
long RxErrors,
|
||||
long TxErrors
|
||||
);
|
||||
|
||||
// Network
|
||||
private record RawNetLine(
|
||||
string Iface,
|
||||
long RxBytes,
|
||||
long RxPackets,
|
||||
long RxErrors,
|
||||
long TxBytes,
|
||||
long TxPackets,
|
||||
long TxErrors
|
||||
);
|
||||
|
||||
private static async Task<List<RawNetLine>> ReadRawNetStatsAsync()
|
||||
{
|
||||
var lines = await File.ReadAllLinesAsync("/proc/net/dev");
|
||||
var result = new List<RawNetLine>();
|
||||
|
||||
// The first two lines are the column-header banner.
|
||||
foreach (var line in lines.Skip(2))
|
||||
{
|
||||
var colon = line.IndexOf(':');
|
||||
if (colon < 0)
|
||||
continue;
|
||||
|
||||
var iface = line[..colon].Trim();
|
||||
|
||||
// Skip loopback and ephemeral veth pairs created by container runtimes.
|
||||
if (iface == "lo" || iface.StartsWith("veth", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
var f = line[(colon + 1)..].Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (f.Length < 16)
|
||||
continue;
|
||||
|
||||
// Column layout (after the colon, 0-based):
|
||||
// RX: [0]bytes [1]packets [2]errs [3]drop [4]fifo [5]frame [6]compressed [7]multicast
|
||||
// TX: [8]bytes [9]packets [10]errs [11]drop [12]fifo [13]colls [14]carrier [15]compressed
|
||||
result.Add(new RawNetLine(
|
||||
Iface: iface,
|
||||
RxBytes: long.Parse(f[0]),
|
||||
RxPackets: long.Parse(f[1]),
|
||||
RxErrors: long.Parse(f[2]),
|
||||
TxBytes: long.Parse(f[8]),
|
||||
TxPackets: long.Parse(f[9]),
|
||||
TxErrors: long.Parse(f[10])
|
||||
));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<NetworkInterfaceInfo> ComputeNetworkRates(
|
||||
List<RawNetLine> s1,
|
||||
List<RawNetLine> s2,
|
||||
double intervalSecs
|
||||
)
|
||||
{
|
||||
var prev = s1.ToDictionary(x => x.Iface);
|
||||
var result = new List<NetworkInterfaceInfo>();
|
||||
var div = intervalSecs > 0 ? intervalSecs : 1.0;
|
||||
|
||||
foreach (var cur in s2)
|
||||
{
|
||||
if (!prev.TryGetValue(cur.Iface, out var p))
|
||||
continue;
|
||||
|
||||
result.Add(new NetworkInterfaceInfo(
|
||||
Name: cur.Iface,
|
||||
RxBytesPerSec: (long)((cur.RxBytes - p.RxBytes) / div),
|
||||
TxBytesPerSec: (long)((cur.TxBytes - p.TxBytes) / div),
|
||||
RxPacketsPerSec: (long)((cur.RxPackets - p.RxPackets) / div),
|
||||
TxPacketsPerSec: (long)((cur.TxPackets - p.TxPackets) / div),
|
||||
RxErrors: cur.RxErrors,
|
||||
TxErrors: cur.TxErrors
|
||||
));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
59
MoonlightServers.Daemon/Helpers/SystemMetrics.cs
Normal file
59
MoonlightServers.Daemon/Helpers/SystemMetrics.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
namespace MoonlightServers.Daemon.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Reads Linux system metrics directly from the <c>/proc</c> and <c>/sys</c>
|
||||
/// pseudo-filesystems
|
||||
/// </summary>
|
||||
public static partial class SystemMetrics
|
||||
{
|
||||
public record SystemSnapshot(
|
||||
CpuSnapshot Cpu,
|
||||
MemoryInfo Memory,
|
||||
IReadOnlyList<DiskInfo> Disks,
|
||||
IReadOnlyList<NetworkInterfaceInfo> Network,
|
||||
TimeSpan Uptime
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Collects a full system snapshot. The method waits <paramref name="sampleIntervalMs"/>
|
||||
/// milliseconds between the two samples required to compute CPU and network rates.
|
||||
/// All other reads happen in parallel during the second sampling window.
|
||||
/// </summary>
|
||||
/// <param name="sampleIntervalMs">
|
||||
/// Interval between rate-measurement samples in milliseconds.
|
||||
/// Larger values yield smoother CPU and network averages. Defaults to <c>500</c>.
|
||||
/// </param>
|
||||
/// <returns>A fully populated <see cref="SystemSnapshot"/>.</returns>
|
||||
public static async Task<SystemSnapshot> ReadAllAsync(int sampleIntervalMs = 500)
|
||||
{
|
||||
// First samples — must complete before the delay.
|
||||
var cpuSample1Task = ReadRawCpuStatsAsync();
|
||||
var netSample1Task = ReadRawNetStatsAsync();
|
||||
await Task.WhenAll(cpuSample1Task, netSample1Task);
|
||||
|
||||
await Task.Delay(sampleIntervalMs);
|
||||
|
||||
// Second samples + all independent reads run concurrently.
|
||||
var cpuSample2Task = ReadRawCpuStatsAsync();
|
||||
var netSample2Task = ReadRawNetStatsAsync();
|
||||
var memTask = ReadMemoryAsync();
|
||||
var diskTask = ReadDisksAsync();
|
||||
var uptimeTask = ReadUptimeAsync();
|
||||
var cpuNameTask = ReadCpuModelNameAsync();
|
||||
|
||||
await Task.WhenAll(cpuSample2Task, netSample2Task, memTask, diskTask, uptimeTask, cpuNameTask);
|
||||
|
||||
var cpu = ComputeCpuUsage(cpuNameTask.Result, cpuSample1Task.Result, cpuSample2Task.Result);
|
||||
var network = ComputeNetworkRates(netSample1Task.Result, netSample2Task.Result, sampleIntervalMs / 1000.0);
|
||||
|
||||
return new SystemSnapshot(cpu, memTask.Result, diskTask.Result, network, uptimeTask.Result);
|
||||
}
|
||||
|
||||
// Uptime
|
||||
private static async Task<TimeSpan> ReadUptimeAsync()
|
||||
{
|
||||
var text = await File.ReadAllTextAsync("/proc/uptime");
|
||||
var seconds = double.Parse(text.Split(' ')[0], System.Globalization.CultureInfo.InvariantCulture);
|
||||
return TimeSpan.FromSeconds(seconds);
|
||||
}
|
||||
}
|
||||
22
MoonlightServers.Daemon/Http/Controllers/SystemController.cs
Normal file
22
MoonlightServers.Daemon/Http/Controllers/SystemController.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MoonlightServers.Daemon.Helpers;
|
||||
using MoonlightServers.Daemon.Mappers;
|
||||
using MoonlightServers.DaemonShared.Http.Daemon;
|
||||
|
||||
namespace MoonlightServers.Daemon.Http.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/system")]
|
||||
public class SystemController : Controller
|
||||
{
|
||||
[HttpGet("statistics")]
|
||||
public async Task<ActionResult<SystemStatisticsDto>> GetStatisticsAsync()
|
||||
{
|
||||
var snapshot = await SystemMetrics.ReadAllAsync();
|
||||
var statistics = SystemStatisticsMapper.ToDto(snapshot);
|
||||
|
||||
return statistics;
|
||||
}
|
||||
}
|
||||
14
MoonlightServers.Daemon/Mappers/SystemStatisticsMapper.cs
Normal file
14
MoonlightServers.Daemon/Mappers/SystemStatisticsMapper.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MoonlightServers.Daemon.Helpers;
|
||||
using MoonlightServers.DaemonShared.Http.Daemon;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.Mappers;
|
||||
|
||||
[Mapper]
|
||||
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
|
||||
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
|
||||
public static partial class SystemStatisticsMapper
|
||||
{
|
||||
public static partial SystemStatisticsDto ToDto(SystemMetrics.SystemSnapshot snapshot);
|
||||
}
|
||||
@@ -4,10 +4,12 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Docker.DotNet.Enhanced" Version="3.132.0" />
|
||||
<PackageReference Include="Riok.Mapperly" Version="5.0.0-next.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -26,6 +28,18 @@
|
||||
<Compile Update="ServerSystem\Server.Update.cs">
|
||||
<DependentUpon>Server.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Helpers\SystemMetrics.Cpu.cs">
|
||||
<DependentUpon>SystemMetrics.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Helpers\SystemMetrics.Memory.cs">
|
||||
<DependentUpon>SystemMetrics.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Helpers\SystemMetrics.Network.cs">
|
||||
<DependentUpon>SystemMetrics.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Helpers\SystemMetrics.Disk.cs">
|
||||
<DependentUpon>SystemMetrics.cs</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
namespace MoonlightServers.DaemonShared.Http.Daemon;
|
||||
|
||||
public record SystemStatisticsDto(
|
||||
CpuSnapshotDto Cpu,
|
||||
MemoryInfoDto Memory,
|
||||
IReadOnlyList<DiskInfoDto> Disks,
|
||||
IReadOnlyList<NetworkInterfaceInfoDto> Network,
|
||||
TimeSpan Uptime
|
||||
);
|
||||
|
||||
public record CpuSnapshotDto(
|
||||
string ModelName,
|
||||
double TotalUsagePercent,
|
||||
IReadOnlyList<double> CoreUsagePercents
|
||||
);
|
||||
|
||||
public record MemoryInfoDto(
|
||||
long TotalBytes,
|
||||
long UsedBytes,
|
||||
long FreeBytes,
|
||||
long CachedBytes,
|
||||
long BuffersBytes,
|
||||
long AvailableBytes,
|
||||
double UsedPercent
|
||||
);
|
||||
|
||||
public record DiskInfoDto(
|
||||
string MountPoint,
|
||||
string Device,
|
||||
string FileSystem,
|
||||
long TotalBytes,
|
||||
long UsedBytes,
|
||||
long FreeBytes,
|
||||
double UsedPercent,
|
||||
long InodesTotal,
|
||||
long InodesUsed,
|
||||
long InodesFree,
|
||||
double InodesUsedPercent
|
||||
);
|
||||
|
||||
public record NetworkInterfaceInfoDto(
|
||||
string Name,
|
||||
long RxBytesPerSec,
|
||||
long TxBytesPerSec,
|
||||
long RxPacketsPerSec,
|
||||
long TxPacketsPerSec,
|
||||
long RxErrors,
|
||||
long TxErrors
|
||||
);
|
||||
@@ -5,6 +5,7 @@ using MoonlightServers.DaemonShared.Http.Daemon;
|
||||
namespace MoonlightServers.DaemonShared.Http;
|
||||
|
||||
[JsonSerializable(typeof(HealthDto))]
|
||||
[JsonSerializable(typeof(SystemStatisticsDto))]
|
||||
[JsonSerializable(typeof(ProblemDetails))]
|
||||
|
||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
|
||||
|
||||
@@ -6,6 +6,18 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Nuget Package Settings">
|
||||
<Version>2.1.0</Version>
|
||||
<Title>MoonlightServers.DaemonShared</Title>
|
||||
<Authors>Moonlight Panel</Authors>
|
||||
<Description>Development package of MoonlightServers.DaemonShared</Description>
|
||||
<Copyright>Moonlight Panel</Copyright>
|
||||
<PackageProjectUrl>https://git.battlestati.one/Moonlight-Panel/Servers</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://git.battlestati.one/Moonlight-Panel/Servers/src/branch/v2.1/LICENSE</PackageLicenseUrl>
|
||||
<RepositoryUrl>https://git.battlestati.one/Moonlight-Panel/Servers</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Http\Panel\" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/admin/servers/nodes/create"
|
||||
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Nodes
|
||||
@using ShadcnBlazor.Buttons
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
@page "/admin/servers/nodes/{Id:int}"
|
||||
|
||||
@using System.Net
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Nodes
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Emptys
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject NavigationManager Navigation
|
||||
@inject ToastService ToastService
|
||||
|
||||
<LazyLoader Load="LoadAsync">
|
||||
@if (Node == null)
|
||||
{
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||
<SearchIcon/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Node not found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
A node with this id cannot be found
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
}
|
||||
else
|
||||
{
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="formContext">
|
||||
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-xl font-semibold">Update Node</h1>
|
||||
<div class="text-muted-foreground">
|
||||
Update node @Node.Name
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-1.5">
|
||||
<Button Variant="ButtonVariant.Secondary">
|
||||
<Slot>
|
||||
<a href="/admin/servers?tab=nodes" @attributes="context">
|
||||
<ChevronLeftIcon/>
|
||||
Back
|
||||
</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
<SubmitButton>
|
||||
<CheckIcon/>
|
||||
Continue
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<DataAnnotationsValidator/>
|
||||
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||
<Field>
|
||||
<FieldLabel for="nodeName">Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Name"
|
||||
id="nodeName"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="nodeHttpEndpoint">HTTP Endpoint</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.HttpEndpointUrl"
|
||||
id="nodeHttpEndpoint"
|
||||
placeholder="http://example.com:8080"/>
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
</EnhancedEditForm>
|
||||
}
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public int Id { get; set; }
|
||||
|
||||
private UpdateNodeDto Request;
|
||||
private NodeDto? Node;
|
||||
|
||||
private async Task LoadAsync(LazyLoader _)
|
||||
{
|
||||
var response = await HttpClient.GetAsync($"api/admin/servers/nodes/{Id}");
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
if(response.StatusCode == HttpStatusCode.NotFound)
|
||||
return;
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
return;
|
||||
}
|
||||
|
||||
Node = await response.Content.ReadFromJsonAsync<NodeDto>(SerializationContext.Default.Options);
|
||||
|
||||
if(Node == null)
|
||||
return;
|
||||
|
||||
Request = new UpdateNodeDto()
|
||||
{
|
||||
Name = Node.Name,
|
||||
HttpEndpointUrl = Node.HttpEndpointUrl
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<bool> OnSubmitAsync(EditContext context, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
var response = await HttpClient.PutAsJsonAsync(
|
||||
$"/api/admin/servers/nodes/{Id}",
|
||||
Request,
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Node Update",
|
||||
$"Successfully updated node {Request.Name}"
|
||||
);
|
||||
|
||||
Navigation.NavigateTo("/admin/servers?tab=nodes");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
@using Microsoft.Extensions.Logging
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Nodes
|
||||
@using ShadcnBlazor.Tooltips
|
||||
|
||||
@@ -55,7 +56,7 @@ else
|
||||
|
||||
try
|
||||
{
|
||||
var result = await HttpClient.GetFromJsonAsync<NodeHealthDto>($"api/admin/servers/nodes/{Node.Id}/health");
|
||||
var result = await HttpClient.GetFromJsonAsync<NodeHealthDto>($"api/admin/servers/nodes/{Node.Id}/health", SerializationContext.Default.Options);
|
||||
|
||||
if(result == null)
|
||||
return;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Http.Requests
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Shared
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Nodes
|
||||
@using ShadcnBlazor.DataGrids
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Dropdowns
|
||||
@using ShadcnBlazor.Extras.AlertDialogs
|
||||
@using ShadcnBlazor.Extras.Dialogs
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Tabels
|
||||
|
||||
|
||||
85
MoonlightServers.Frontend/Admin/Nodes/SettingsTab.razor
Normal file
85
MoonlightServers.Frontend/Admin/Nodes/SettingsTab.razor
Normal file
@@ -0,0 +1,85 @@
|
||||
@using Moonlight.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Nodes
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="formContext">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<DataAnnotationsValidator/>
|
||||
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||
<Field>
|
||||
<FieldLabel for="nodeName">Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Name"
|
||||
id="nodeName"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="nodeHttpEndpoint">HTTP Endpoint</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.HttpEndpointUrl"
|
||||
id="nodeHttpEndpoint"
|
||||
placeholder="http://example.com:8080"/>
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter ClassName="justify-end">
|
||||
<SubmitButton>Save changes</SubmitButton>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</EnhancedEditForm>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter, EditorRequired] public NodeDto Node { get; set; }
|
||||
|
||||
private UpdateNodeDto Request;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Request = new UpdateNodeDto()
|
||||
{
|
||||
Name = Node.Name,
|
||||
HttpEndpointUrl = Node.HttpEndpointUrl
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<bool> OnSubmitAsync(EditContext context, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
var response = await HttpClient.PutAsJsonAsync(
|
||||
$"/api/admin/servers/nodes/{Node.Id}",
|
||||
Request,
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Node Update",
|
||||
$"Successfully updated node {Request.Name}"
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
301
MoonlightServers.Frontend/Admin/Nodes/StatisticsTab.razor
Normal file
301
MoonlightServers.Frontend/Admin/Nodes/StatisticsTab.razor
Normal file
@@ -0,0 +1,301 @@
|
||||
@using LucideBlazor
|
||||
@using MoonlightServers.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Frontend.Shared
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Nodes
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Emptys
|
||||
@using ShadcnBlazor.Spinners
|
||||
@using ShadcnBlazor.Progresses
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<h3 class="text-base font-semibold mt-5 mb-2">Overview</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5">
|
||||
<div class="col-span-1">
|
||||
<RealtimeChart @ref="CpuChart" T="double"
|
||||
Title="CPU Usage"
|
||||
DisplayField="@(d => $"{Math.Round(d, 2)}%")"
|
||||
ValueField="d => d"
|
||||
ClassName="h-32"/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1">
|
||||
<RealtimeChart @ref="MemoryChart" T="MemoryDataPoint"
|
||||
Title="Memory Usage"
|
||||
DisplayField="@(d => Formatter.FormatSize(d.UsedMemory))"
|
||||
ValueField="d => d.Percent"
|
||||
ClassName="h-32"/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1">
|
||||
<RealtimeChart @ref="NetworkInChart" T="long"
|
||||
Title="Incoming Traffic"
|
||||
DisplayField="@(d => $"{Formatter.FormatSize(d)}/s")"
|
||||
Min="0"
|
||||
Max="512"
|
||||
ValueField="@(d => d / 1024f / 1024f)"
|
||||
ClassName="h-32"/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1">
|
||||
<RealtimeChart @ref="NetworkOutChart" T="long"
|
||||
Title="Outgoing Traffic"
|
||||
DisplayField="@(d => $"{Formatter.FormatSize(d)}/s")"
|
||||
Min="0"
|
||||
Max="512"
|
||||
ValueField="@(d => d / 1024f / 1024f)"
|
||||
ClassName="h-32"/>
|
||||
</div>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
@if (HasLoaded && StatisticsDto != null)
|
||||
{
|
||||
<CardHeader ClassName="gap-0">
|
||||
<CardDescription>Uptime</CardDescription>
|
||||
<CardTitle ClassName="text-lg">@Formatter.FormatDuration(StatisticsDto.Uptime)</CardTitle>
|
||||
</CardHeader>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
@if (HasLoaded && HealthDto != null)
|
||||
{
|
||||
<CardHeader ClassName="gap-0">
|
||||
<CardDescription>Health Status</CardDescription>
|
||||
<CardTitle ClassName="text-lg">
|
||||
@if (HealthDto.IsHealthy)
|
||||
{
|
||||
<span class="text-green-500">Healthy</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-destructive">Unhealthy</span>
|
||||
}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<h3 class="text-base font-semibold mt-8 mb-2">Details</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>System Details</CardTitle>
|
||||
<CardDescription>Details over your general system configuration</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@if (StatisticsDto == null)
|
||||
{
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||
<CpuIcon/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No details</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
No details about your system found
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground font-medium">CPU Model</span>
|
||||
<span>
|
||||
@StatisticsDto.Cpu.ModelName
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground font-medium">Total Memory</span>
|
||||
<span>
|
||||
@Formatter.FormatSize(StatisticsDto.Memory.TotalBytes)
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground font-medium">Total Disk Space</span>
|
||||
<span>
|
||||
@{
|
||||
var totalDiskSpace = StatisticsDto
|
||||
.Disks
|
||||
.DistinctBy(x => x.Device)
|
||||
.Sum(x => x.TotalBytes);
|
||||
}
|
||||
|
||||
@Formatter.FormatSize(totalDiskSpace)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Disk Details</CardTitle>
|
||||
<CardDescription>Details over all your mounted disks</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@if (StatisticsDto == null)
|
||||
{
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||
<HardDriveIcon/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No details</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
No details about disk and their usage found
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="space-y-4">
|
||||
@foreach (var disk in StatisticsDto.Disks)
|
||||
{
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium">
|
||||
@disk.MountPoint
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
@disk.FileSystem
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
@disk.Device
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
@Formatter.FormatSize(disk.UsedBytes) / @Formatter.FormatSize(disk.TotalBytes)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Progress Value="(int)disk.UsedPercent" Max="100"></Progress>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public NodeDto Node { get; set; }
|
||||
|
||||
private NodeHealthDto? HealthDto;
|
||||
private NodeStatisticsDto? StatisticsDto;
|
||||
|
||||
private bool HasLoaded;
|
||||
|
||||
private RealtimeChart<double>? CpuChart;
|
||||
|
||||
private RealtimeChart<long>? NetworkInChart;
|
||||
|
||||
private RealtimeChart<long>? NetworkOutChart;
|
||||
|
||||
private RealtimeChart<MemoryDataPoint>? MemoryChart;
|
||||
private Timer? Timer;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender) return;
|
||||
|
||||
HealthDto = await HttpClient.GetFromJsonAsync<NodeHealthDto>(
|
||||
$"api/admin/servers/nodes/{Node.Id}/health",
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (HealthDto is { IsHealthy: true })
|
||||
{
|
||||
StatisticsDto = await HttpClient.GetFromJsonAsync<NodeStatisticsDto>(
|
||||
$"api/admin/servers/nodes/{Node.Id}/statistics",
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
}
|
||||
|
||||
HasLoaded = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
Timer = new Timer(RefreshCallbackAsync, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
private async void RefreshCallbackAsync(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatisticsDto = await HttpClient.GetFromJsonAsync<NodeStatisticsDto>(
|
||||
$"api/admin/servers/nodes/{Node.Id}/statistics",
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (StatisticsDto == null) return;
|
||||
|
||||
if (CpuChart != null)
|
||||
await CpuChart.PushAsync(StatisticsDto.Cpu.TotalUsagePercent);
|
||||
|
||||
if (MemoryChart != null)
|
||||
await MemoryChart.PushAsync(new MemoryDataPoint(StatisticsDto.Memory.UsedBytes, StatisticsDto.Memory.UsedPercent));
|
||||
|
||||
if (NetworkInChart != null && NetworkOutChart != null)
|
||||
{
|
||||
var networkInterface = StatisticsDto
|
||||
.Network
|
||||
.FirstOrDefault(x => x.Name.StartsWith("eth"));
|
||||
|
||||
if (networkInterface == null)
|
||||
{
|
||||
networkInterface = StatisticsDto
|
||||
.Network
|
||||
.FirstOrDefault(x => x.Name.StartsWith("en"));
|
||||
}
|
||||
|
||||
if (networkInterface == null)
|
||||
return;
|
||||
|
||||
await NetworkInChart.PushAsync(networkInterface.RxBytesPerSec);
|
||||
await NetworkOutChart.PushAsync(networkInterface.TxBytesPerSec);
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (Timer != null)
|
||||
await Timer.DisposeAsync();
|
||||
|
||||
if (CpuChart != null)
|
||||
await CpuChart.DisposeAsync();
|
||||
}
|
||||
|
||||
private record MemoryDataPoint(long UsedMemory, double Percent);
|
||||
}
|
||||
90
MoonlightServers.Frontend/Admin/Nodes/View.razor
Normal file
90
MoonlightServers.Frontend/Admin/Nodes/View.razor
Normal file
@@ -0,0 +1,90 @@
|
||||
@page "/admin/servers/nodes/{Id:int}"
|
||||
|
||||
@using LucideBlazor
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Nodes
|
||||
@using ShadcnBlazor.Emptys
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Tab
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
|
||||
@attribute [Authorize(Policy = Permissions.Nodes.View)]
|
||||
|
||||
<LazyLoader Load="LoadAsync">
|
||||
@if (Dto == null)
|
||||
{
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||
<SearchIcon/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Node not found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
A node with this id cannot be found
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-xl font-semibold">@Dto.Name</h1>
|
||||
<div class="text-muted-foreground">
|
||||
View details for @Dto.Name
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-1.5">
|
||||
<Button Variant="ButtonVariant.Secondary">
|
||||
<Slot>
|
||||
<a href="/admin/servers?tab=nodes" @attributes="context">
|
||||
<ChevronLeftIcon/>
|
||||
Back
|
||||
</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<Tabs DefaultValue="statistics">
|
||||
<TabsList ClassName="inline-flex w-full lg:w-fit justify-start overflow-x-auto overflow-y-hidden">
|
||||
<TabsTrigger Value="statistics">
|
||||
<ChartColumnBigIcon/>
|
||||
Statistics
|
||||
</TabsTrigger>
|
||||
<TabsTrigger Value="settings">
|
||||
<SettingsIcon/>
|
||||
Settings
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent Value="statistics">
|
||||
<StatisticsTab Node="Dto" />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent Value="settings">
|
||||
<SettingsTab Node="Dto" />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
</div>
|
||||
}
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public int Id { get; set; }
|
||||
|
||||
private NodeDto? Dto;
|
||||
|
||||
private async Task LoadAsync(LazyLoader _)
|
||||
{
|
||||
Dto = await HttpClient.GetFromJsonAsync<NodeDto>(
|
||||
$"api/admin/servers/nodes/{Id}",
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/admin/servers/templates/create"
|
||||
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Buttons
|
||||
@@ -10,7 +10,6 @@
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Tab
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Dialogs
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Dialogs
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@using System.Text.Json
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Shared
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@@ -206,7 +206,7 @@
|
||||
try
|
||||
{
|
||||
var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>(
|
||||
Moonlight.Shared.Http.SerializationContext.Default.Options
|
||||
Moonlight.Shared.SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (problemDetails == null)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Http.Requests
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Shared
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.DataGrids
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
@page "/admin/servers/templates/{Id:int}"
|
||||
|
||||
@using System.Net
|
||||
@using System.Text.Json
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Buttons
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Accordions
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Accordions
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.Dialogs
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject DialogService DialogService
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace MoonlightServers.Frontend.Infrastructure.Helpers;
|
||||
|
||||
internal static class Formatter
|
||||
{
|
||||
internal static string FormatSize(long bytes, double conversionStep = 1024)
|
||||
{
|
||||
if (bytes == 0) return "0 B";
|
||||
|
||||
string[] units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
|
||||
var unitIndex = 0;
|
||||
double size = bytes;
|
||||
|
||||
while (size >= conversionStep && unitIndex < units.Length - 1)
|
||||
{
|
||||
size /= conversionStep;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
var decimals = unitIndex == 0 ? 0 : 2;
|
||||
return $"{Math.Round(size, decimals)} {units[unitIndex]}";
|
||||
}
|
||||
|
||||
internal static string FormatDuration(TimeSpan timeSpan)
|
||||
{
|
||||
var abs = timeSpan.Duration(); // Handle negative timespans
|
||||
|
||||
if (abs.TotalSeconds < 1)
|
||||
return $"{abs.TotalMilliseconds:F0}ms";
|
||||
|
||||
if (abs.TotalMinutes < 1)
|
||||
return $"{abs.TotalSeconds:F1}s";
|
||||
|
||||
if (abs.TotalHours < 1)
|
||||
return $"{abs.Minutes}m {abs.Seconds}s";
|
||||
|
||||
if (abs.TotalDays < 1)
|
||||
return $"{abs.Hours}h {abs.Minutes}m";
|
||||
|
||||
if (abs.TotalDays < 365)
|
||||
return $"{abs.Days}d {abs.Hours}h";
|
||||
|
||||
var years = (int)(abs.TotalDays / 365);
|
||||
var days = abs.Days % 365;
|
||||
return days > 0 ? $"{years}y {days}d" : $"{years}y";
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using LucideBlazor;
|
||||
using Moonlight.Frontend.Interfaces;
|
||||
using Moonlight.Frontend.Models;
|
||||
using Moonlight.Frontend.Admin.Users.Shared;
|
||||
using Moonlight.Frontend.Infrastructure.Hooks;
|
||||
using MoonlightServers.Shared;
|
||||
|
||||
namespace MoonlightServers.Frontend.Infrastructure;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
@inherits Moonlight.Frontend.Infrastructure.Hooks.LayoutMiddlewareBase
|
||||
|
||||
@ChildContent
|
||||
|
||||
<script src="/_content/MoonlightServers.Frontend/realtimeChart.js"></script>
|
||||
@@ -1,6 +1,6 @@
|
||||
using LucideBlazor;
|
||||
using Moonlight.Frontend.Interfaces;
|
||||
using Moonlight.Frontend.Models;
|
||||
using Moonlight.Frontend.Infrastructure.Hooks;
|
||||
using Moonlight.Frontend.Infrastructure.Models;
|
||||
using MoonlightServers.Shared;
|
||||
|
||||
namespace MoonlightServers.Frontend.Infrastructure;
|
||||
|
||||
@@ -6,6 +6,17 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Nuget Package Settings">
|
||||
<Version>2.1.0</Version>
|
||||
<Title>MoonlightServers.Frontend</Title>
|
||||
<Authors>Moonlight Panel</Authors>
|
||||
<Description>Development package of MoonlightServers.Frontend</Description>
|
||||
<Copyright>Moonlight Panel</Copyright>
|
||||
<PackageProjectUrl>https://git.battlestati.one/Moonlight-Panel/Servers</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://git.battlestati.one/Moonlight-Panel/Servers/src/branch/v2.1/LICENSE</PackageLicenseUrl>
|
||||
<RepositoryUrl>https://git.battlestati.one/Moonlight-Panel/Servers</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<SupportedPlatform Include="browser"/>
|
||||
@@ -18,7 +29,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Client\" />
|
||||
<Folder Include="wwwroot\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -27,8 +37,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Styles/*" Pack="true" PackagePath="Styles/"/>
|
||||
<None Include="Servers.Frontend.targets" Pack="true" PackagePath="build\Servers.Frontend.targets"/>
|
||||
<None Include="Servers.Frontend.targets" Pack="true" PackagePath="buildTransitive\Servers.Frontend.targets"/>
|
||||
<None Include="MoonlightServers.Frontend.targets" Pack="true" PackagePath="build\MoonlightServers.Frontend.targets" />
|
||||
<None Include="MoonlightServers.Frontend.targets" Pack="true" PackagePath="buildTransitive\MoonlightServers.Frontend.targets"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
15
MoonlightServers.Frontend/MoonlightServers.Frontend.targets
Normal file
15
MoonlightServers.Frontend/MoonlightServers.Frontend.targets
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<MoonlightServersCssClassDir Condition="'$(MoonlightServersCssClassDir)' == ''">
|
||||
$(MSBuildProjectDirectory)\bin\MoonlightServers
|
||||
</MoonlightServersCssClassDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="MoonlightServers_CopyContents" BeforeTargets="Build">
|
||||
<ItemGroup>
|
||||
<Files Include="$(MSBuildThisFileDirectory)..\Styles\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<Copy SourceFiles="@(Files)" DestinationFolder="$(MoonlightServersCssClassDir)" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -1,15 +0,0 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ServersCssClassDir Condition="'$(ServersCssClassDir)' == ''">
|
||||
$(MSBuildProjectDirectory)\bin\Servers
|
||||
</ServersCssClassDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="Servers_CopyContents" BeforeTargets="Build">
|
||||
<ItemGroup>
|
||||
<Files Include="$(MSBuildThisFileDirectory)..\Styles\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<Copy SourceFiles="@(Files)" DestinationFolder="$(ServersCssClassDir)" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
</Project>
|
||||
93
MoonlightServers.Frontend/Shared/RealtimeChart.razor
Normal file
93
MoonlightServers.Frontend/Shared/RealtimeChart.razor
Normal file
@@ -0,0 +1,93 @@
|
||||
@using ShadcnBlazor.Cards
|
||||
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
@implements IAsyncDisposable
|
||||
|
||||
@typeparam T
|
||||
|
||||
<Card ClassName="py-0 overflow-hidden">
|
||||
<CardContent ClassName="@($"px-0 relative overflow-hidden {ClassName}")">
|
||||
<div class="absolute top-6 left-6 z-10">
|
||||
@if (!string.IsNullOrEmpty(Title))
|
||||
{
|
||||
<CardDescription>@Title</CardDescription>
|
||||
}
|
||||
<CardTitle ClassName="text-lg">
|
||||
@if (CurrentValue != null)
|
||||
{
|
||||
@DisplayField.Invoke(CurrentValue)
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>-</span>
|
||||
}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<canvas id="@Identifier" class="absolute block rounded-xl -left-5 -right-5 top-0 -bottom-2 w-[calc(100%+30px)]! h-[calc(100%+8px)]!">
|
||||
</canvas>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public IEnumerable<T>? DefaultItems { get; set; }
|
||||
[Parameter] public Func<T, string> DisplayField { get; set; }
|
||||
[Parameter] public Func<T, double> ValueField { get; set; }
|
||||
[Parameter] public string Title { get; set; }
|
||||
[Parameter] public int Min { get; set; } = 0;
|
||||
[Parameter] public int Max { get; set; } = 100;
|
||||
[Parameter] public int VisibleDataPoints { get; set; } = 30;
|
||||
|
||||
[Parameter] public string ClassName { get; set; }
|
||||
|
||||
private string Identifier;
|
||||
private T? CurrentValue;
|
||||
private int Counter;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Identifier = $"realtimeChart{GetHashCode()}";
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender) return;
|
||||
|
||||
var items = DefaultItems?.ToArray() ?? [];
|
||||
|
||||
var labels = items.Select(x =>
|
||||
{
|
||||
Counter++;
|
||||
return Counter.ToString();
|
||||
});
|
||||
|
||||
var dataPoints = items.Select(ValueField);
|
||||
|
||||
await JsRuntime.InvokeVoidAsync(
|
||||
"moonlightServersRealtimeChart.init",
|
||||
Identifier,
|
||||
Identifier,
|
||||
VisibleDataPoints,
|
||||
Min,
|
||||
Max,
|
||||
labels,
|
||||
dataPoints
|
||||
);
|
||||
}
|
||||
|
||||
public async Task PushAsync(T value)
|
||||
{
|
||||
Counter++;
|
||||
var label = Counter.ToString();
|
||||
var dataPoint = ValueField.Invoke(value);
|
||||
|
||||
CurrentValue = value;
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("moonlightServersRealtimeChart.pushValue", Identifier, label, dataPoint);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
=> await JsRuntime.InvokeVoidAsync("moonlightServersRealtimeChart.destroy", Identifier);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moonlight.Frontend;
|
||||
using Moonlight.Frontend.Configuration;
|
||||
using Moonlight.Frontend.Interfaces;
|
||||
using Moonlight.Frontend.Infrastructure.Configuration;
|
||||
using Moonlight.Frontend.Infrastructure.Hooks;
|
||||
using MoonlightServers.Frontend.Infrastructure;
|
||||
using SimplePlugin.Abstractions;
|
||||
|
||||
@@ -20,6 +20,11 @@ public sealed class Startup : MoonlightPlugin
|
||||
{
|
||||
options.Assemblies.Add(typeof(Startup).Assembly);
|
||||
});
|
||||
|
||||
builder.Services.Configure<LayoutMiddlewareOptions>(options =>
|
||||
{
|
||||
options.Add<ScriptImports>();
|
||||
});
|
||||
}
|
||||
|
||||
public override void PostBuild(WebAssemblyHost application)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
THIS WILL BE AUTOGENERATED DURING PACKAGE BUILD
|
||||
85
MoonlightServers.Frontend/wwwroot/realtimeChart.js
Normal file
85
MoonlightServers.Frontend/wwwroot/realtimeChart.js
Normal file
@@ -0,0 +1,85 @@
|
||||
window.moonlightServersRealtimeChart = {
|
||||
instances: new Map(),
|
||||
init: function (id, elementId, maxDataPoints, minY, maxY, defaultLabels, defaultDataPoints) {
|
||||
const canvas = document.getElementById(elementId);
|
||||
|
||||
const labels = [];
|
||||
labels.push(... defaultLabels);
|
||||
|
||||
const dataPoints = [];
|
||||
dataPoints.push(... defaultDataPoints);
|
||||
|
||||
const chart = new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
data: dataPoints,
|
||||
borderColor: 'oklch(0.58 0.18 270)',
|
||||
backgroundColor: 'rgba(55,138,221,0.15)',
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 400,
|
||||
easing: 'easeInOutCubic'
|
||||
},
|
||||
layout: {padding: 0},
|
||||
plugins: {legend: {display: false}, tooltip: {enabled: false}},
|
||||
scales: {
|
||||
x: {display: false},
|
||||
y: {display: false, min: minY, max: maxY}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.instances.set(id, {
|
||||
chart: chart,
|
||||
labels: labels,
|
||||
dataPoints: dataPoints,
|
||||
maxDataPoints: maxDataPoints
|
||||
});
|
||||
},
|
||||
pushValue: function (id, label, val) {
|
||||
const chartData = this.instances.get(id);
|
||||
const isShifting = chartData.labels.length >= chartData.maxDataPoints;
|
||||
|
||||
chartData.labels.push(label);
|
||||
chartData.dataPoints.push(val);
|
||||
|
||||
if (isShifting) {
|
||||
// Animate the new point drawing in first...
|
||||
chartData.chart.update({
|
||||
duration: 300,
|
||||
easing: 'easeOutCubic',
|
||||
lazy: false
|
||||
});
|
||||
|
||||
// ...then silently trim the oldest point after the animation completes
|
||||
setTimeout(() => {
|
||||
chartData.labels.shift();
|
||||
chartData.dataPoints.shift();
|
||||
chartData.chart.update('none');
|
||||
}, 300);
|
||||
} else {
|
||||
chartData.chart.update({
|
||||
duration: 500,
|
||||
easing: 'easeOutQuart',
|
||||
lazy: false
|
||||
});
|
||||
}
|
||||
},
|
||||
destroy: function (id) {
|
||||
const chartData = this.instances.get(id);
|
||||
|
||||
chartData.chart.destroy();
|
||||
|
||||
this.instances.delete(id);
|
||||
}
|
||||
}
|
||||
49
MoonlightServers.Shared/Admin/Nodes/NodeStatisticsDto.cs
Normal file
49
MoonlightServers.Shared/Admin/Nodes/NodeStatisticsDto.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
namespace MoonlightServers.Shared.Admin.Nodes;
|
||||
|
||||
public record NodeStatisticsDto(
|
||||
CpuSnapshotDto Cpu,
|
||||
MemoryInfoDto Memory,
|
||||
IReadOnlyList<DiskInfoDto> Disks,
|
||||
IReadOnlyList<NetworkInterfaceInfoDto> Network,
|
||||
TimeSpan Uptime
|
||||
);
|
||||
|
||||
public record CpuSnapshotDto(
|
||||
string ModelName,
|
||||
double TotalUsagePercent,
|
||||
IReadOnlyList<double> CoreUsagePercents
|
||||
);
|
||||
|
||||
public record MemoryInfoDto(
|
||||
long TotalBytes,
|
||||
long UsedBytes,
|
||||
long FreeBytes,
|
||||
long CachedBytes,
|
||||
long BuffersBytes,
|
||||
long AvailableBytes,
|
||||
double UsedPercent
|
||||
);
|
||||
|
||||
public record DiskInfoDto(
|
||||
string MountPoint,
|
||||
string Device,
|
||||
string FileSystem,
|
||||
long TotalBytes,
|
||||
long UsedBytes,
|
||||
long FreeBytes,
|
||||
double UsedPercent,
|
||||
long InodesTotal,
|
||||
long InodesUsed,
|
||||
long InodesFree,
|
||||
double InodesUsedPercent
|
||||
);
|
||||
|
||||
public record NetworkInterfaceInfoDto(
|
||||
string Name,
|
||||
long RxBytesPerSec,
|
||||
long TxBytesPerSec,
|
||||
long RxPacketsPerSec,
|
||||
long TxPacketsPerSec,
|
||||
long RxErrors,
|
||||
long TxErrors
|
||||
);
|
||||
@@ -6,6 +6,18 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Nuget Package Settings">
|
||||
<Version>2.1.0</Version>
|
||||
<Title>MoonlightServers.Shared</Title>
|
||||
<Authors>Moonlight Panel</Authors>
|
||||
<Description>Development package of MoonlightServers.Shared</Description>
|
||||
<Copyright>Moonlight Panel</Copyright>
|
||||
<PackageProjectUrl>https://git.battlestati.one/Moonlight-Panel/Servers</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://git.battlestati.one/Moonlight-Panel/Servers/src/branch/v2.1/LICENSE</PackageLicenseUrl>
|
||||
<RepositoryUrl>https://git.battlestati.one/Moonlight-Panel/Servers</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moonlight.Shared" Version="2.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Shared;
|
||||
using MoonlightServers.Shared.Admin.Nodes;
|
||||
using MoonlightServers.Shared.Admin.Templates;
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace MoonlightServers.Shared;
|
||||
// - Node
|
||||
[JsonSerializable(typeof(CreateNodeDto))]
|
||||
[JsonSerializable(typeof(UpdateNodeDto))]
|
||||
[JsonSerializable(typeof(NodeHealthDto))]
|
||||
[JsonSerializable(typeof(NodeStatisticsDto))]
|
||||
[JsonSerializable(typeof(NodeDto))]
|
||||
[JsonSerializable(typeof(PagedData<NodeDto>))]
|
||||
|
||||
@@ -21,19 +23,18 @@ namespace MoonlightServers.Shared;
|
||||
[JsonSerializable(typeof(DetailedTemplateDto))]
|
||||
[JsonSerializable(typeof(PagedData<TemplateDto>))]
|
||||
|
||||
[JsonSerializable(typeof(VariableDto))]
|
||||
[JsonSerializable(typeof(PagedData<VariableDto>))]
|
||||
[JsonSerializable(typeof(CreateVariableDto))]
|
||||
[JsonSerializable(typeof(UpdateVariableDto))]
|
||||
// - Template - Variables
|
||||
[JsonSerializable(typeof(VariableDto))]
|
||||
[JsonSerializable(typeof(PagedData<VariableDto>))]
|
||||
[JsonSerializable(typeof(CreateVariableDto))]
|
||||
[JsonSerializable(typeof(UpdateVariableDto))]
|
||||
|
||||
// - Template - Docker Image
|
||||
[JsonSerializable(typeof(DockerImageDto))]
|
||||
[JsonSerializable(typeof(PagedData<DockerImageDto>))]
|
||||
[JsonSerializable(typeof(CreateDockerImageDto))]
|
||||
[JsonSerializable(typeof(UpdateDockerImageDto))]
|
||||
|
||||
|
||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
|
||||
public partial class SerializationContext : JsonSerializerContext
|
||||
{
|
||||
|
||||
}
|
||||
@@ -7,7 +7,9 @@
|
||||
<Project Path="Hosts\MoonlightServers.Api.Host\MoonlightServers.Api.Host.csproj" />
|
||||
<Project Path="Hosts\MoonlightServers.Frontend.Host\MoonlightServers.Frontend.Host.csproj" />
|
||||
</Folder>
|
||||
<Project Path="MoonlightServers.Api\MoonlightServers.Api.csproj" />
|
||||
<Project Path="MoonlightServers.Frontend\MoonlightServers.Frontend.csproj" />
|
||||
<Project Path="MoonlightServers.Shared\MoonlightServers.Shared.csproj" />
|
||||
<Folder Name="/Panel/">
|
||||
<Project Path="MoonlightServers.Api\MoonlightServers.Api.csproj" />
|
||||
<Project Path="MoonlightServers.Frontend\MoonlightServers.Frontend.csproj" />
|
||||
<Project Path="MoonlightServers.Shared\MoonlightServers.Shared.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
Reference in New Issue
Block a user