From 89662df42fbe8b137ba86f43986a44a4ed5f86bc Mon Sep 17 00:00:00 2001 From: mx57 <38256814+mx57@users.noreply.github.com> Date: Fri, 11 Sep 2026 20:18:49 +0000 Subject: [PATCH] Add log file download feature to Web Dashboard Implement DownloadLog controller endpoint in HomeController.cs allowing users to stream active general or trade log files using FileShare.ReadWrite. Add download buttons to Live Log Monitor and Live Trade Logs Terminal cards in Dashboard.cshtml, dynamically synchronizing download links in dashboard.js. Add unit tests in HomeControllerTests.cs and update agent_tasks.json. --- .../HomeControllerTests.cs | 115 ++++++++++++++++++ .../IntelliTrader.Trading.Tests.csproj | 1 + .../Controllers/HomeController.cs | 23 ++++ .../Static/Scripts/Views/dashboard.js | 2 + IntelliTrader.Web/Views/Home/Dashboard.cshtml | 21 +++- magda_agent_system/agent_tasks.json | 18 ++- 6 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 IntelliTrader.Trading.Tests/HomeControllerTests.cs diff --git a/IntelliTrader.Trading.Tests/HomeControllerTests.cs b/IntelliTrader.Trading.Tests/HomeControllerTests.cs new file mode 100644 index 0000000..1c4d149 --- /dev/null +++ b/IntelliTrader.Trading.Tests/HomeControllerTests.cs @@ -0,0 +1,115 @@ +using System; +using System.IO; +using System.Text; +using IntelliTrader.Web.Controllers; +using Microsoft.AspNetCore.Mvc; +using Xunit; + +namespace IntelliTrader.Trading.Tests +{ + public class HomeControllerTests + { + [Fact] + public void DownloadLog_ReturnsNotFound_WhenNoMatchingFile() + { + // Arrange + var controller = new HomeController(); + + // Act + var result = controller.DownloadLog("nonexistent_log_type_12345"); + + // Assert + Assert.IsType(result); + var notFoundResult = (NotFoundObjectResult)result; + Assert.Equal("Log file not found.", notFoundResult.Value); + } + + [Fact] + public void DownloadLog_ReturnsFileStreamResult_WhenGeneralLogExists() + { + // Arrange + var controller = new HomeController(); + string logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "log"); + if (!Directory.Exists(logDirectory)) + { + Directory.CreateDirectory(logDirectory); + } + + string testFileName = $"{DateTime.Now:yyyy-MM-dd-HHmmss}-test-general.txt"; + string testFilePath = Path.Combine(logDirectory, testFileName); + string testContent = "[INF] 2026-08-12 10:00:00 Test log entry for DownloadLog test."; + + try + { + File.WriteAllText(testFilePath, testContent, Encoding.UTF8); + + // Act + var result = controller.DownloadLog("general"); + + // Assert + Assert.IsType(result); + var fileResult = (FileStreamResult)result; + Assert.Equal("text/plain", fileResult.ContentType); + Assert.False(string.IsNullOrEmpty(fileResult.FileDownloadName)); + Assert.EndsWith("-general.txt", fileResult.FileDownloadName); + + using (var reader = new StreamReader(fileResult.FileStream, Encoding.UTF8)) + { + string content = reader.ReadToEnd(); + Assert.Contains("Test log entry for DownloadLog test.", content); + } + } + finally + { + if (File.Exists(testFilePath)) + { + File.Delete(testFilePath); + } + } + } + + [Fact] + public void DownloadLog_ReturnsFileStreamResult_WhenTradesLogExists() + { + // Arrange + var controller = new HomeController(); + string logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "log"); + if (!Directory.Exists(logDirectory)) + { + Directory.CreateDirectory(logDirectory); + } + + string testFileName = $"{DateTime.Now:yyyy-MM-dd-HHmmss}-test-trades.txt"; + string testFilePath = Path.Combine(logDirectory, testFileName); + string testContent = "TradeResult {\"Pair\":\"BTCUSDT\",\"Profit\":10.5}"; + + try + { + File.WriteAllText(testFilePath, testContent, Encoding.UTF8); + + // Act + var result = controller.DownloadLog("trades"); + + // Assert + Assert.IsType(result); + var fileResult = (FileStreamResult)result; + Assert.Equal("text/plain", fileResult.ContentType); + Assert.False(string.IsNullOrEmpty(fileResult.FileDownloadName)); + Assert.EndsWith("-trades.txt", fileResult.FileDownloadName); + + using (var reader = new StreamReader(fileResult.FileStream, Encoding.UTF8)) + { + string content = reader.ReadToEnd(); + Assert.Contains("TradeResult", content); + } + } + finally + { + if (File.Exists(testFilePath)) + { + File.Delete(testFilePath); + } + } + } + } +} diff --git a/IntelliTrader.Trading.Tests/IntelliTrader.Trading.Tests.csproj b/IntelliTrader.Trading.Tests/IntelliTrader.Trading.Tests.csproj index 3d88629..6d1e7bb 100644 --- a/IntelliTrader.Trading.Tests/IntelliTrader.Trading.Tests.csproj +++ b/IntelliTrader.Trading.Tests/IntelliTrader.Trading.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/IntelliTrader.Web/Controllers/HomeController.cs b/IntelliTrader.Web/Controllers/HomeController.cs index 27edbbc..c76b7a3 100644 --- a/IntelliTrader.Web/Controllers/HomeController.cs +++ b/IntelliTrader.Web/Controllers/HomeController.cs @@ -538,6 +538,29 @@ public IActionResult Help(string lang = "en") return View(model); } + [HttpGet] + public IActionResult DownloadLog(string type = "general") + { + try + { + string pattern = "general".Equals(type, StringComparison.OrdinalIgnoreCase) ? "*-general.txt" : "*-trades.txt"; + string filePath = GetLatestLogFilePath(pattern); + + if (string.IsNullOrEmpty(filePath) || !System.IO.File.Exists(filePath)) + { + return NotFound("Log file not found."); + } + + var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + string fileName = Path.GetFileName(filePath); + return File(fileStream, "text/plain", fileName); + } + catch (Exception ex) + { + return BadRequest($"Unable to download log file: {ex.Message}"); + } + } + [HttpGet] public IActionResult PollLogs(string type = "general", int maxLines = 100) { diff --git a/IntelliTrader.Web/Static/Scripts/Views/dashboard.js b/IntelliTrader.Web/Static/Scripts/Views/dashboard.js index 1a2d24f..b8fa523 100644 --- a/IntelliTrader.Web/Static/Scripts/Views/dashboard.js +++ b/IntelliTrader.Web/Static/Scripts/Views/dashboard.js @@ -205,6 +205,8 @@ function setLogType(type) { $("#logTypeGeneralBtn").removeClass("active"); } + $("#downloadLogMonitorBtn").attr("href", "/Home/DownloadLog?type=" + type); + $("#logTerminal").html('
Loading logs...
'); pollLiveLogs(); } diff --git a/IntelliTrader.Web/Views/Home/Dashboard.cshtml b/IntelliTrader.Web/Views/Home/Dashboard.cshtml index 29406e5..7c4601d 100644 --- a/IntelliTrader.Web/Views/Home/Dashboard.cshtml +++ b/IntelliTrader.Web/Views/Home/Dashboard.cshtml @@ -457,9 +457,14 @@
Live Log Monitor
-
- - +
+
+ + +
+ + Скачать лог +
@@ -566,7 +571,10 @@ - + + + Скачать лог +
@@ -634,7 +642,12 @@ const intervalId = setInterval(fetchLogs, 3000); logTypeSelect.addEventListener("change", function() { + const logType = logTypeSelect.value; lastLogContent = ""; + const downloadBtn = document.getElementById("downloadTradeLogBtn"); + if (downloadBtn) { + downloadBtn.href = "/Home/DownloadLog?type=" + logType; + } logTerminal.innerHTML = '
Loading logs...
'; fetchLogs(); }); diff --git a/magda_agent_system/agent_tasks.json b/magda_agent_system/agent_tasks.json index 42a30c7..2eead1f 100644 --- a/magda_agent_system/agent_tasks.json +++ b/magda_agent_system/agent_tasks.json @@ -476,7 +476,7 @@ }, { "id": "web-dashboard-log-download", - "status": "todo", + "status": "done", "area": "web", "risk": "low", "title": "Add log file download button on Web Dashboard", @@ -505,6 +505,22 @@ "acceptance": [ "Users can click export on the Stats page to download summary statistics." ] + }, + { + "id": "web-dashboard-table-filtering", + "status": "todo", + "area": "web", + "risk": "low", + "title": "Enhance trading pair table search and filtering options", + "description": "Add quick status filters (e.g. Trailing Buys, Trailing Sells, Active DCA) to the Dashboard trading pair DataTables.", + "allowed_paths": [ + "IntelliTrader.Web/Views/**", + "IntelliTrader.Web/Static/**", + "magda_agent_system/agent_tasks.json" + ], + "acceptance": [ + "Dashboard UI provides quick filter buttons to filter active trading pairs by status." + ] } ] }