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()