diff --git a/Moonlight.Api/Http/Controllers/Admin/DiagnoseController.cs b/Moonlight.Api/Http/Controllers/Admin/DiagnoseController.cs new file mode 100644 index 00000000..acff03f9 --- /dev/null +++ b/Moonlight.Api/Http/Controllers/Admin/DiagnoseController.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Mvc; +using Moonlight.Api.Mappers; +using Moonlight.Api.Services; +using Moonlight.Shared.Http.Responses.Admin; + +namespace Moonlight.Api.Http.Controllers.Admin; + +[ApiController] +[Route("api/admin/system/diagnose")] +public class DiagnoseController : Controller +{ + private readonly DiagnoseService DiagnoseService; + + public DiagnoseController(DiagnoseService diagnoseService) + { + DiagnoseService = diagnoseService; + } + + [HttpGet] + public async Task> GetAsync() + { + var results = await DiagnoseService.DiagnoseAsync(); + + return results + .OrderBy(x => x.Level) + .MapToResult() + .ToArray(); + } +} \ No newline at end of file diff --git a/Moonlight.Api/Implementations/UpdateDiagnoseProvider.cs b/Moonlight.Api/Implementations/UpdateDiagnoseProvider.cs new file mode 100644 index 00000000..cf1b99e8 --- /dev/null +++ b/Moonlight.Api/Implementations/UpdateDiagnoseProvider.cs @@ -0,0 +1,33 @@ +using Moonlight.Api.Interfaces; +using Moonlight.Api.Models; +using Moonlight.Api.Services; + +namespace Moonlight.Api.Implementations; + +public sealed class UpdateDiagnoseProvider : IDiagnoseProvider +{ + private readonly ApplicationService ApplicationService; + + public UpdateDiagnoseProvider(ApplicationService applicationService) + { + ApplicationService = applicationService; + } + + public Task DiagnoseAsync() + { + if (ApplicationService.IsUpToDate) + return Task.FromResult([]); + + return Task.FromResult([ + new DiagnoseResult( + DiagnoseLevel.Warning, + "Instance is not up-to-date", + ["Moonlight", "Update Check"], + "Update your moonlight instance to receive bug fixes, new features and security patches. Update button can be found in the overview", + null, + "/admin", + null + ) + ]); + } +} \ No newline at end of file diff --git a/Moonlight.Api/Interfaces/IDiagnoseProvider.cs b/Moonlight.Api/Interfaces/IDiagnoseProvider.cs new file mode 100644 index 00000000..e5d0a4a6 --- /dev/null +++ b/Moonlight.Api/Interfaces/IDiagnoseProvider.cs @@ -0,0 +1,8 @@ +using Moonlight.Api.Models; + +namespace Moonlight.Api.Interfaces; + +public interface IDiagnoseProvider +{ + public Task DiagnoseAsync(); +} \ No newline at end of file diff --git a/Moonlight.Api/Mappers/DiagnoseResultMapper.cs b/Moonlight.Api/Mappers/DiagnoseResultMapper.cs new file mode 100644 index 00000000..b5a6ad2b --- /dev/null +++ b/Moonlight.Api/Mappers/DiagnoseResultMapper.cs @@ -0,0 +1,14 @@ +using System.Diagnostics.CodeAnalysis; +using Moonlight.Api.Models; +using Moonlight.Shared.Http.Responses.Admin; +using Riok.Mapperly.Abstractions; + +namespace Moonlight.Api.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 DiagnoseResultMapper +{ + public static partial IEnumerable MapToResult(this IEnumerable results); +} \ No newline at end of file diff --git a/Moonlight.Api/Models/DiagnoseResult.cs b/Moonlight.Api/Models/DiagnoseResult.cs new file mode 100644 index 00000000..c968ab92 --- /dev/null +++ b/Moonlight.Api/Models/DiagnoseResult.cs @@ -0,0 +1,10 @@ +namespace Moonlight.Api.Models; + +public record DiagnoseResult(DiagnoseLevel Level, string Title, string[] Tags, string? Message, string? StackStrace, string? SolutionUrl, string? ReportUrl); + +public enum DiagnoseLevel +{ + Error = 0, + Warning = 1, + Healthy = 2 +} \ No newline at end of file diff --git a/Moonlight.Api/Services/DiagnoseService.cs b/Moonlight.Api/Services/DiagnoseService.cs new file mode 100644 index 00000000..0cfdf77b --- /dev/null +++ b/Moonlight.Api/Services/DiagnoseService.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Logging; +using Moonlight.Api.Interfaces; +using Moonlight.Api.Models; + +namespace Moonlight.Api.Services; + +public class DiagnoseService +{ + private readonly IEnumerable Providers; + private readonly ILogger Logger; + + public DiagnoseService(IEnumerable providers, ILogger logger) + { + Providers = providers; + Logger = logger; + } + + public async Task DiagnoseAsync() + { + var results = new List(); + + foreach (var provider in Providers) + { + try + { + results.AddRange( + await provider.DiagnoseAsync() + ); + } + catch (Exception e) + { + Logger.LogError(e, "An unhandled error occured while processing provider"); + } + } + + return results.ToArray(); + } +} \ No newline at end of file diff --git a/Moonlight.Api/Startup/Startup.Base.cs b/Moonlight.Api/Startup/Startup.Base.cs index 4d417ec3..0e50dc93 100644 --- a/Moonlight.Api/Startup/Startup.Base.cs +++ b/Moonlight.Api/Startup/Startup.Base.cs @@ -4,6 +4,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; using Moonlight.Shared.Http; using Moonlight.Api.Helpers; +using Moonlight.Api.Implementations; +using Moonlight.Api.Interfaces; using Moonlight.Api.Services; namespace Moonlight.Api.Startup; @@ -23,6 +25,10 @@ public partial class Startup builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); + + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); } private static void UseBase(WebApplication application) diff --git a/Moonlight.Frontend/UI/Admin/Views/Settings/Diagnose.razor b/Moonlight.Frontend/UI/Admin/Views/Settings/Diagnose.razor new file mode 100644 index 00000000..96f2c427 --- /dev/null +++ b/Moonlight.Frontend/UI/Admin/Views/Settings/Diagnose.razor @@ -0,0 +1,228 @@ +@using LucideBlazor +@using Moonlight.Shared.Http.Responses.Admin +@using ShadcnBlazor.Accordions +@using ShadcnBlazor.Alerts +@using ShadcnBlazor.Buttons +@using ShadcnBlazor.Cards +@using ShadcnBlazor.Emptys +@using ShadcnBlazor.Extras.Common +@using ShadcnBlazor.Spinners + +@inject HttpClient HttpClient + +
+
+ + + Automatic diagnosis + + Use a diagnostic report to share configuration details and errors with Moonlight developers, with + sensitive data automatically censored. + + + + +
+ +
+
+
+ Notice + + Only share these reports with the moonlight developers or the corresponding plugin + developers. + Even though we do our best to censor sensitive data it may still contain information you + dont want a random person on the internet to know + +
+
+
+
+ + + + Start diagnostics + + +
+
+
+ + + @if (IsLoading) + { + + } + else + { + if (HasDiagnosed) + { + if (Entries.Length == 0) + { + + + + + + No results available + + Diagnosis didnt return any results + + + + } + else + { + + + @for (var i = 0; i < Entries.Length; i++) + { + var entry = Entries[i]; + + var textColor = entry.Level switch + { + DiagnoseLevel.Error => "text-destructive", + DiagnoseLevel.Warning => "text-yellow-300", + DiagnoseLevel.Healthy => "text-green-500" + }; + + + + +
+ + @switch (entry.Level) + { + case DiagnoseLevel.Error: + + break; + + case DiagnoseLevel.Warning: + + break; + + case DiagnoseLevel.Healthy: + + break; + } + +
+ + @entry.Title + + + @(string.Join(" / ", entry.Tags)) + +
+
+
+ +
+ + @if (!string.IsNullOrWhiteSpace(entry.StackStrace)) + { +
+ @entry.StackStrace +
+ } + + @if (!string.IsNullOrWhiteSpace(entry.Message)) + { +

+ @entry.Message +

+ } + + @if ( + !string.IsNullOrWhiteSpace(entry.ReportUrl) || + !string.IsNullOrWhiteSpace(entry.StackStrace) || + !string.IsNullOrWhiteSpace(entry.SolutionUrl) + ) + { +
+ @if (!string.IsNullOrWhiteSpace(entry.StackStrace)) + { + + } + + @if (!string.IsNullOrWhiteSpace(entry.SolutionUrl)) + { + + } + + @if (!string.IsNullOrWhiteSpace(entry.ReportUrl)) + { + + } +
+ } +
+
+
+ } +
+ } + } + else + { + + + + + + No results available yet + + Press the start button to start the automatic diagnosis + + + + } + } +
+
+
+
+ +@code +{ + private bool IsLoading = false; + private bool HasDiagnosed = false; + private DiagnoseResultResponse[] Entries; + + private async Task DiagnoseAsync() + { + IsLoading = true; + HasDiagnosed = false; + await InvokeAsync(StateHasChanged); + + var results = await HttpClient.GetFromJsonAsync("api/admin/system/diagnose"); + Entries = results ?? []; + + IsLoading = false; + HasDiagnosed = true; + await InvokeAsync(StateHasChanged); + } +} diff --git a/Moonlight.Frontend/UI/Admin/Views/Settings/Index.razor b/Moonlight.Frontend/UI/Admin/Views/Settings/Index.razor index a7288a7a..863a4b1b 100644 --- a/Moonlight.Frontend/UI/Admin/Views/Settings/Index.razor +++ b/Moonlight.Frontend/UI/Admin/Views/Settings/Index.razor @@ -43,4 +43,7 @@ + + + \ No newline at end of file diff --git a/Moonlight.Shared/Http/Responses/Admin/DiagnoseResultResponse.cs b/Moonlight.Shared/Http/Responses/Admin/DiagnoseResultResponse.cs new file mode 100644 index 00000000..87971662 --- /dev/null +++ b/Moonlight.Shared/Http/Responses/Admin/DiagnoseResultResponse.cs @@ -0,0 +1,10 @@ +namespace Moonlight.Shared.Http.Responses.Admin; + +public record DiagnoseResultResponse(DiagnoseLevel Level, string Title, string[] Tags, string? Message, string? StackStrace, string? SolutionUrl, string? ReportUrl); + +public enum DiagnoseLevel +{ + Error = 0, + Warning = 1, + Healthy = 2 +} \ No newline at end of file diff --git a/Moonlight.Shared/Http/SerializationContext.cs b/Moonlight.Shared/Http/SerializationContext.cs index 73e451ab..5d73740d 100644 --- a/Moonlight.Shared/Http/SerializationContext.cs +++ b/Moonlight.Shared/Http/SerializationContext.cs @@ -11,6 +11,7 @@ namespace Moonlight.Shared.Http; [JsonSerializable(typeof(UpdateUserRequest))] [JsonSerializable(typeof(ClaimResponse[]))] [JsonSerializable(typeof(SchemeResponse[]))] +[JsonSerializable(typeof(DiagnoseResultResponse[]))] [JsonSerializable(typeof(UserResponse))] [JsonSerializable(typeof(SystemInfoResponse))] [JsonSerializable(typeof(PagedData))]