From e2f344ab4e2d3a1fa20592427e71dc0f7ad70426 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Fri, 23 Jan 2026 16:38:42 +0100 Subject: [PATCH] Added container rebuild flow with real-time logs and updated UI, backend implementation, config options, and container helper API integration. --- .../Configuration/ContainerHelperOptions.cs | 7 + .../Http/Controllers/Admin/ChController.cs | 27 +++ .../Http/Controllers/PingController.cs | 13 ++ Moonlight.Api/Services/ApplicationService.cs | 2 +- .../Services/ContainerHelperService.cs | 67 ++++++++ Moonlight.Api/Startup/Startup.Base.cs | 9 + .../UI/Admin/Modals/UpdateInstanceModal.razor | 157 ++++++++++++++---- .../UI/Admin/Views/Overview.razor | 1 + Moonlight.Shared/Http/Events/RebuildEvent.cs | 20 +++ Moonlight.Shared/Http/SerializationContext.cs | 4 + compose.yaml | 30 +++- 11 files changed, 300 insertions(+), 37 deletions(-) create mode 100644 Moonlight.Api/Configuration/ContainerHelperOptions.cs create mode 100644 Moonlight.Api/Http/Controllers/Admin/ChController.cs create mode 100644 Moonlight.Api/Http/Controllers/PingController.cs create mode 100644 Moonlight.Api/Services/ContainerHelperService.cs create mode 100644 Moonlight.Shared/Http/Events/RebuildEvent.cs diff --git a/Moonlight.Api/Configuration/ContainerHelperOptions.cs b/Moonlight.Api/Configuration/ContainerHelperOptions.cs new file mode 100644 index 00000000..42f89bd7 --- /dev/null +++ b/Moonlight.Api/Configuration/ContainerHelperOptions.cs @@ -0,0 +1,7 @@ +namespace Moonlight.Api.Configuration; + +public class ContainerHelperOptions +{ + public bool IsEnabled { get; set; } + public string Url { get; set; } = "http://helper:8080"; +} \ No newline at end of file diff --git a/Moonlight.Api/Http/Controllers/Admin/ChController.cs b/Moonlight.Api/Http/Controllers/Admin/ChController.cs new file mode 100644 index 00000000..4fe148df --- /dev/null +++ b/Moonlight.Api/Http/Controllers/Admin/ChController.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moonlight.Api.Services; + +namespace Moonlight.Api.Http.Controllers.Admin; + +[ApiController] +[Route("api/admin/ch")] +public class ChController : Controller +{ + private readonly ContainerHelperService ContainerHelperService; + + public ChController(ContainerHelperService containerHelperService) + { + ContainerHelperService = containerHelperService; + } + + [HttpPost("rebuild")] + public Task RebuildAsync() + { + var result = ContainerHelperService.RebuildAsync(); + + return Task.FromResult( + TypedResults.ServerSentEvents(result) + ); + } +} \ No newline at end of file diff --git a/Moonlight.Api/Http/Controllers/PingController.cs b/Moonlight.Api/Http/Controllers/PingController.cs new file mode 100644 index 00000000..8de46960 --- /dev/null +++ b/Moonlight.Api/Http/Controllers/PingController.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Moonlight.Api.Http.Controllers; + +[ApiController] +[Route("api/ping")] +public class PingController : Controller +{ + [HttpGet] + [AllowAnonymous] + public IActionResult Get() => Ok("Pong"); +} \ No newline at end of file diff --git a/Moonlight.Api/Services/ApplicationService.cs b/Moonlight.Api/Services/ApplicationService.cs index 3a002ab1..d72952ff 100644 --- a/Moonlight.Api/Services/ApplicationService.cs +++ b/Moonlight.Api/Services/ApplicationService.cs @@ -47,7 +47,7 @@ public class ApplicationService : IHostedLifecycleService // TODO: Update / version check VersionName = "v2.1.0 (a2d4edc0e5)"; - IsUpToDate = true; + IsUpToDate = false; OperatingSystem = OsHelper.GetName(); } diff --git a/Moonlight.Api/Services/ContainerHelperService.cs b/Moonlight.Api/Services/ContainerHelperService.cs new file mode 100644 index 00000000..578fc4ba --- /dev/null +++ b/Moonlight.Api/Services/ContainerHelperService.cs @@ -0,0 +1,67 @@ +using System.Text.Json; +using Moonlight.Shared.Http; +using Moonlight.Shared.Http.Events; + +namespace Moonlight.Api.Services; + +public class ContainerHelperService +{ + private readonly IHttpClientFactory HttpClientFactory; + + public ContainerHelperService(IHttpClientFactory httpClientFactory) + { + HttpClientFactory = httpClientFactory; + } + + public async IAsyncEnumerable RebuildAsync() + { + var options = new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true + }; + + options.TypeInfoResolverChain.Add(SerializationContext.Default); + + var client = HttpClientFactory.CreateClient("ContainerHelper"); + + var response = await client.GetAsync("api/rebuild", HttpCompletionOption.ResponseHeadersRead); + + if (!response.IsSuccessStatusCode) + { + var responseText = await response.Content.ReadAsStringAsync(); + yield return new RebuildEvent() + { + Type = RebuildEventType.Failed, + Data = responseText + }; + + yield break; + } + + await using var responseStream = await response.Content.ReadAsStreamAsync(); + using var streamReader = new StreamReader(responseStream); + + do + { + var line = await streamReader.ReadLineAsync(); + + if(line == null) + break; + + if(string.IsNullOrWhiteSpace(line)) + continue; + + var data = line.Trim("data: "); + + var deserializedData = JsonSerializer.Deserialize(data, options); + + yield return deserializedData; + } while (true); + + yield return new RebuildEvent() + { + Type = RebuildEventType.Succeeded, + Data = string.Empty + }; + } +} \ No newline at end of file diff --git a/Moonlight.Api/Startup/Startup.Base.cs b/Moonlight.Api/Startup/Startup.Base.cs index 147f6a48..25c97ea6 100644 --- a/Moonlight.Api/Startup/Startup.Base.cs +++ b/Moonlight.Api/Startup/Startup.Base.cs @@ -38,6 +38,15 @@ public partial class Startup builder.Services.AddOptions().BindConfiguration("Moonlight:Frontend"); builder.Services.AddScoped(); + + builder.Services.AddOptions().BindConfiguration("Moonlight:ContainerHelper"); + builder.Services.AddSingleton(); + + builder.Services.AddHttpClient("ContainerHelper", (provider, client) => + { + var options = provider.GetRequiredService>(); + client.BaseAddress = new Uri(options.Value.IsEnabled ? options.Value.Url : "http://you-should-fail.invalid"); + }); } private static void UseBase(WebApplication application) diff --git a/Moonlight.Frontend/UI/Admin/Modals/UpdateInstanceModal.razor b/Moonlight.Frontend/UI/Admin/Modals/UpdateInstanceModal.razor index ba8e6dab..68f9eb96 100644 --- a/Moonlight.Frontend/UI/Admin/Modals/UpdateInstanceModal.razor +++ b/Moonlight.Frontend/UI/Admin/Modals/UpdateInstanceModal.razor @@ -1,37 +1,31 @@ @inherits ShadcnBlazor.Extras.Dialogs.DialogBase +@using System.Text.Json @using LucideBlazor +@using Moonlight.Shared.Http +@using Moonlight.Shared.Http.Events @using ShadcnBlazor.Dialogs @using ShadcnBlazor.Extras.AlertDialogs @using ShadcnBlazor.Progresses @using ShadcnBlazor.Spinners @inject AlertDialogService AlertService +@inject HttpClient HttpClient - Updating... + Updating instance... -
- @for (var i = 0; i < Steps.Length; i++) - { - if (CurrentStep == i) +
+
+ @for (var i = 0; i < Steps.Length; i++) { -
- - - @Steps[i] - -
- } - else - { - if (i < CurrentStep) + if (CurrentStep == i) {
- + @Steps[i] @@ -39,13 +33,33 @@ } else { -
- - @Steps[i] -
+ if (i < CurrentStep) + { +
+ + + @Steps[i] + +
+ } + else + { +
+ + @Steps[i] +
+ } } } - } +
+
+ @for (var i = LogLines.Count - 1; i >= 0; i--) + { +
+ @LogLines[i] +
+ } +
@@ -62,12 +76,15 @@ [ "Preparing", "Updating configuration files", + "Starting rebuild task", "Building docker image", "Redeploying container instance", "Waiting for container instance to start up", "Update complete" ]; + private List LogLines = new(); + protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) @@ -83,30 +100,100 @@ Progress = 20; await InvokeAsync(StateHasChanged); - await Task.Delay(6000); - - CurrentStep = 2; - Progress = 40; - await InvokeAsync(StateHasChanged); - await Task.Delay(2000); - CurrentStep = 3; - Progress = 60; + CurrentStep = 2; + Progress = 30; await InvokeAsync(StateHasChanged); - await Task.Delay(4000); + var response = await HttpClient.SendAsync( + new HttpRequestMessage(HttpMethod.Post, "api/admin/ch/rebuild"), + HttpCompletionOption.ResponseHeadersRead + ); - CurrentStep = 4; - Progress = 80; - await InvokeAsync(StateHasChanged); + await using var responseStream = await response.Content.ReadAsStreamAsync(); + using var streamReader = new StreamReader(responseStream); - await Task.Delay(4000); + do + { + try + { + var line = await streamReader.ReadLineAsync(); + + if (line == null) + break; + + if (string.IsNullOrWhiteSpace(line)) + continue; + + var data = line.Trim("data: "); + var deserializedData = JsonSerializer.Deserialize(data, Constants.SerializerOptions); + + switch (deserializedData.Type) + { + case RebuildEventType.Log: + LogLines.Add(deserializedData.Data); + break; + + case RebuildEventType.Step: + + switch (deserializedData.Data) + { + case "BuildImage": + CurrentStep = 3; + Progress = 40; + await InvokeAsync(StateHasChanged); + break; + + case "ServiceDown": + CurrentStep = 4; + Progress = 60; + await InvokeAsync(StateHasChanged); + break; + + case "ServiceUp": + CurrentStep = 4; + Progress = 80; + await InvokeAsync(StateHasChanged); + break; + } + + break; + } + + await InvokeAsync(StateHasChanged); + } + catch (Exception e) + { + // TODO: Log + break; + } + } while (true); CurrentStep = 5; + Progress = 90; + await InvokeAsync(StateHasChanged); + + // Ping instance until its reachable again + while (true) + { + try + { + await HttpClient.GetStringAsync("api/ping"); + break; + } + catch (Exception) + { + // Ignored + } + + await Task.Delay(3000); + } + + CurrentStep = 6; Progress = 100; await InvokeAsync(StateHasChanged); - + await Task.Delay(1000); await AlertService.SuccessAsync( diff --git a/Moonlight.Frontend/UI/Admin/Views/Overview.razor b/Moonlight.Frontend/UI/Admin/Views/Overview.razor index ddea7a73..f86db322 100644 --- a/Moonlight.Frontend/UI/Admin/Views/Overview.razor +++ b/Moonlight.Frontend/UI/Admin/Views/Overview.razor @@ -160,5 +160,6 @@ private async Task LaunchUpdateModalAsync() => await DialogService.LaunchAsync(onConfigure: model => { model.ShowCloseButton = false; + model.ClassName = "sm:max-w-4xl!"; }); } diff --git a/Moonlight.Shared/Http/Events/RebuildEvent.cs b/Moonlight.Shared/Http/Events/RebuildEvent.cs new file mode 100644 index 00000000..c3edab82 --- /dev/null +++ b/Moonlight.Shared/Http/Events/RebuildEvent.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace Moonlight.Shared.Http.Events; + +public struct RebuildEvent +{ + [JsonPropertyName("type")] + public RebuildEventType Type { get; set; } + + [JsonPropertyName("data")] + public string Data { get; set; } +} + +public enum RebuildEventType +{ + Log = 0, + Failed = 1, + Succeeded = 2, + Step = 3 +} \ No newline at end of file diff --git a/Moonlight.Shared/Http/SerializationContext.cs b/Moonlight.Shared/Http/SerializationContext.cs index e3739a64..dfe8de60 100644 --- a/Moonlight.Shared/Http/SerializationContext.cs +++ b/Moonlight.Shared/Http/SerializationContext.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using Moonlight.Shared.Http.Events; using Moonlight.Shared.Http.Requests.ApiKeys; using Moonlight.Shared.Http.Requests.Roles; using Moonlight.Shared.Http.Requests.Themes; @@ -43,6 +44,9 @@ namespace Moonlight.Shared.Http; [JsonSerializable(typeof(UpdateThemeDto))] [JsonSerializable(typeof(PagedData))] [JsonSerializable(typeof(ThemeDto))] + +// Events +[JsonSerializable(typeof(RebuildEvent))] public partial class SerializationContext : JsonSerializerContext { } \ No newline at end of file diff --git a/compose.yaml b/compose.yaml index 3f80e03b..f19057b5 100644 --- a/compose.yaml +++ b/compose.yaml @@ -36,4 +36,32 @@ # Logging - "Logging__LogLevel__Default=Information" - - "Logging__LogLevel__Microsoft.AspNetCore=Warning" \ No newline at end of file + - "Logging__LogLevel__Microsoft.AspNetCore=Warning" + - "Logging__LogLevel__System.Net.Http.HttpClient=Warning" + + - "Moonlight__ContainerHelper__IsEnabled=true" + - "Moonlight__ContainerHelper__Url=http://app:8080" + + app: + image: git.battlestati.one/moonlight-panel/container_helper + + group_add: + - "989" + + environment: + # Logging + - "Logging__LogLevel__Default=Information" + - "Logging__LogLevel__Microsoft.AspNetCore=Warning" + + # Compose + - "ContainerHelper__Compose__Directory=${PWD}" + - "ContainerHelper__Compose__Binary=docker-compose" + - "ContainerHelper__Service__Name=api" + + # HTTP Proxy + - "HTTP_PROXY=${HTTP_PROXY}" + - "HTTPS_PROXY=${HTTPS_PROXY}" + + volumes: + - "${PWD}:${PWD}" + - "/var/run/docker.sock:/var/run/docker.sock" \ No newline at end of file