From a9f8961c32d50632ca8b225df92a248f7d0e3b19 Mon Sep 17 00:00:00 2001
From: Alex Hope-O'Connor
Date: Mon, 2 Mar 2026 23:10:39 +1000
Subject: [PATCH] App logs: add Refresh and Download logs buttons; add
api/logging/logs/download endpoint
---
.../Dnsmasq/AppLogs/AppLogsSection.razor | 4 ++++
.../Dnsmasq/AppLogs/AppLogsSection.razor.cs | 20 +++++++++++++++++++
.../Controllers/LoggingController.cs | 19 ++++++++++++++++++
3 files changed, 43 insertions(+)
diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/AppLogs/AppLogsSection.razor b/src/DnsmasqWebUI/Components/Dnsmasq/AppLogs/AppLogsSection.razor
index 26b430f..881126b 100644
--- a/src/DnsmasqWebUI/Components/Dnsmasq/AppLogs/AppLogsSection.razor
+++ b/src/DnsmasqWebUI/Components/Dnsmasq/AppLogs/AppLogsSection.razor
@@ -31,3 +31,7 @@
+
diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/AppLogs/AppLogsSection.razor.cs b/src/DnsmasqWebUI/Components/Dnsmasq/AppLogs/AppLogsSection.razor.cs
index 07040bb..4032232 100644
--- a/src/DnsmasqWebUI/Components/Dnsmasq/AppLogs/AppLogsSection.razor.cs
+++ b/src/DnsmasqWebUI/Components/Dnsmasq/AppLogs/AppLogsSection.razor.cs
@@ -19,6 +19,7 @@ public partial class AppLogsSection : IAsyncDisposable
private bool _justUpdated;
private bool _logsContentReceived;
+ private bool _refreshing;
private int _intervalSeconds;
private string _logLevel = "Information";
private bool _logLevelDisabled;
@@ -91,6 +92,25 @@ public partial class AppLogsSection : IAsyncDisposable
private void OpenFiltersModal() => _filtersModalVisible = true;
private void CloseFiltersModal() => _filtersModalVisible = false;
+ private async Task RequestRefreshAsync()
+ {
+ if (_hubConnection?.State != HubConnectionState.Connected)
+ return;
+ _refreshing = true;
+ StateHasChanged();
+ try
+ {
+ await _hubConnection.InvokeAsync("RequestAppLogsSnapshot", _cts.Token);
+ }
+ catch (OperationCanceledException) { }
+ catch (Exception) { /* ignore */ }
+ finally
+ {
+ _refreshing = false;
+ StateHasChanged();
+ }
+ }
+
private async Task OnLogLevelChanged()
{
if (string.IsNullOrEmpty(_logLevel)) return;
diff --git a/src/DnsmasqWebUI/Controllers/LoggingController.cs b/src/DnsmasqWebUI/Controllers/LoggingController.cs
index c6129ef..37b812f 100644
--- a/src/DnsmasqWebUI/Controllers/LoggingController.cs
+++ b/src/DnsmasqWebUI/Controllers/LoggingController.cs
@@ -1,6 +1,8 @@
+using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using DnsmasqWebUI.Infrastructure.Logging;
+using DnsmasqWebUI.Infrastructure.Services.Logs.Abstractions;
using DnsmasqWebUI.Models.Config;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
@@ -12,22 +14,39 @@ namespace DnsmasqWebUI.Controllers;
[Route("api/[controller]")]
public class LoggingController : ControllerBase
{
+ private const int AppLogsDownloadMaxLines = 5000;
+
private readonly IConfiguration _configuration;
private readonly IConfigurationRoot _configurationRoot;
private readonly RuntimeOverridesOptions _overridesOptions;
+ private readonly IAppLogsBuffer _appLogsBuffer;
private readonly ILogger _logger;
public LoggingController(
IConfiguration configuration,
IOptions overridesOptions,
+ IAppLogsBuffer appLogsBuffer,
ILogger logger)
{
_configuration = configuration;
_configurationRoot = (IConfigurationRoot)configuration;
_overridesOptions = overridesOptions.Value;
+ _appLogsBuffer = appLogsBuffer;
_logger = logger;
}
+ /// Download recent app logs from the in-memory buffer (up to AppLogsDownloadMaxLines).
+ [HttpGet("logs/download")]
+ public IActionResult GetAppLogsDownload()
+ {
+ var lines = _appLogsBuffer.GetRecent(maxLines: AppLogsDownloadMaxLines);
+ var content = string.Join(Environment.NewLine, lines);
+ if (lines.Count > 0 && !content.EndsWith(Environment.NewLine))
+ content += Environment.NewLine;
+ _logger.LogDebug("App logs download, lines={Count}", lines.Count);
+ return File(Encoding.UTF8.GetBytes(content), "text/plain", "app.log");
+ }
+
/// Get the current minimum log level from configuration (Logging:LogLevel:Default).
[HttpGet("level")]
public ActionResult GetLevel()