diff --git a/RobotApp.Client/MainLayout.razor b/RobotApp.Client/MainLayout.razor
index 5aa7765..2823d0a 100644
--- a/RobotApp.Client/MainLayout.razor
+++ b/RobotApp.Client/MainLayout.razor
@@ -59,7 +59,7 @@
-@code{
+@code {
public class NavModel
{
public string Icon { get; set; } = "";
@@ -72,7 +72,8 @@
new(){Icon = "mdi-view-dashboard", Path="/", Label = "Dashboard", Match = NavLinkMatch.All},
// new(){Icon = "mdi-map-legend", Path="/maps-manager", Label = "Mapping", Match = NavLinkMatch.All},
new(){Icon = "mdi-monitor", Path="/robot-monitor", Label = "Robot Monitor", Match = NavLinkMatch.All},
- new(){Icon = "mdi-application-cog", Path="/robot-config", Label = "Config", Match = NavLinkMatch.All},
+ new(){Icon = "mdi-application-cog", Path="/robot-config", Label = "Config", Match = NavLinkMatch.All },
+ new(){Icon = "mdi-math-log", Path="/logs", Label = "Logs", Match = NavLinkMatch.All}
];
private bool collapseNavMenu = true;
diff --git a/RobotApp.Client/Models/LoggerModel.cs b/RobotApp.Client/Models/LoggerModel.cs
new file mode 100644
index 0000000..c8707c0
--- /dev/null
+++ b/RobotApp.Client/Models/LoggerModel.cs
@@ -0,0 +1,40 @@
+using System.Text.Json.Serialization;
+
+namespace RobotApp.Client.Models;
+
+public class LoggerModel
+{
+ [JsonPropertyName("time")]
+ public string? Time { get; set; }
+
+ [JsonPropertyName("level")]
+ public string? Level { get; set; }
+
+ [JsonPropertyName("message")]
+ public string? Message { get; set; }
+
+ [JsonPropertyName("exception")]
+ public string? Exception { get; set; }
+
+ public string ColorClass => Level switch
+ {
+ "WARN" => "text-warning",
+ "INFO" => "text-info",
+ "DEBUG" => "text-success",
+ "ERROR" => "text-danger",
+ "FATAL" => "text-secondary",
+ _ => "text-muted",
+ };
+
+ public string BackgroundClass => Level switch
+ {
+ "WARN" => "bg-warning text-dark",
+ "INFO" => "bg-info text-dark",
+ "DEBUG" => "bg-success text-white",
+ "ERROR" => "bg-danger text-white",
+ "FATAL" => "bg-secondary text-white",
+ _ => "bg-dark text-white",
+ };
+
+ public bool HasException => !string.IsNullOrEmpty(Exception);
+}
diff --git a/RobotApp.Client/Pages/Logs.razor b/RobotApp.Client/Pages/Logs.razor
new file mode 100644
index 0000000..1e18167
--- /dev/null
+++ b/RobotApp.Client/Pages/Logs.razor
@@ -0,0 +1,176 @@
+@page "/logs"
+@rendermode InteractiveWebAssemblyNoPrerender
+@attribute [Authorize]
+
+@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
+@using RobotApp.Client.Models
+
+@inject IJSRuntime JSRuntime
+@inject HttpClient Http
+@inject IConfiguration Configuration
+@inject ISnackbar Snackbar
+
+Logs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (ShowRawLog)
+ {
+
+
+ @foreach (var log in ShowLogs)
+ {
+ @log
+ }
+
+ }
+ else
+ {
+ @if (SearchLogs.Count < ShowLogs.Count)
+ {
+
+ }
+
+ @foreach (var log in SearchLogs)
+ {
+
+
+ @log.Time @log.Level
+
+
@log.Message
+ @if (log.HasException)
+ {
+
+
+ @log.Exception
+
+ }
+
+ }
+ }
+
+
+
+
+
+
+@code {
+ private DateTime DateLog = DateTime.Today;
+ private bool IsLoading;
+ private readonly List ShowLogs = new();
+ private readonly List SearchLogs = new();
+ private ElementReference LogContainerRef { get; set; }
+ private bool ShowRawLog { get; set; }
+ private string? FilterLog { get; set; }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await base.OnAfterRenderAsync(firstRender);
+ if (!firstRender) return;
+
+ await LoadLogs();
+ }
+
+ private async Task LoadLogs()
+ {
+ try
+ {
+ IsLoading = true;
+ ShowLogs.Clear();
+ StateHasChanged();
+
+ var logs = await Http.GetFromJsonAsync>($"api/LogsManager?date={DateLog}");
+ ShowLogs.AddRange(logs ?? []);
+
+ IsLoading = false;
+ StateHasChanged();
+
+ await ReloadLogs();
+ }
+ catch (AccessTokenNotAvailableException ex)
+ {
+ ex.Redirect();
+ return;
+ }
+ }
+
+ private async Task ReloadLogs()
+ {
+ IsLoading = true;
+ SearchLogs.Clear();
+ StateHasChanged();
+
+ foreach (var line in ShowLogs.Where(log => string.IsNullOrEmpty(FilterLog) || log.Contains(FilterLog)).TakeLast(2000))
+ {
+ try
+ {
+ var log = System.Text.Json.JsonSerializer.Deserialize(line);
+ if (log is not null) SearchLogs.Add(log);
+ }
+ catch (System.Text.Json.JsonException)
+ {
+ continue;
+ }
+ }
+
+ IsLoading = false;
+ StateHasChanged();
+ await JSRuntime.InvokeVoidAsync("ScrollToBottom", LogContainerRef);
+ }
+
+ private async Task OnSearch(string text)
+ {
+ FilterLog = text;
+ await ReloadLogs();
+ }
+
+ private async Task OnDateChanged(DateTime? date)
+ {
+ if (date is not null && date.HasValue)
+ {
+ DateLog = date.Value;
+ await LoadLogs();
+ }
+ }
+
+ private async Task ExportLogs()
+ {
+ try
+ {
+ var fileContent = await Http.GetFromJsonAsync>($"api/LogsManager?date={DateLog}");
+ var formattedContent = string.Join("\n", fileContent ?? []);
+ var fileName = $"LogsManager_{DateLog.ToShortDateString()}.txt";
+ await JSRuntime.InvokeVoidAsync("downloadFile", fileName, formattedContent, "text/plain");
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"Lỗi khi tải file: {ex.Message}", Severity.Warning);
+ }
+ }
+}
+
diff --git a/RobotApp.Client/Pages/Logs.razor.css b/RobotApp.Client/Pages/Logs.razor.css
new file mode 100644
index 0000000..2a3ae7a
--- /dev/null
+++ b/RobotApp.Client/Pages/Logs.razor.css
@@ -0,0 +1,38 @@
+.log-container {
+ height: 100%;
+ width: 100%;
+ overflow-x: hidden;
+ overflow-y: auto;
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ display: flex;
+ flex-direction: column;
+}
+
+.log {
+ word-wrap: break-word;
+ line-height: 18px;
+ margin-bottom: 12px;
+}
+
+.log-logger {
+ color: rgba(0, 0, 0, 0.3);
+ font-size: 12px;
+}
+
+.log-level {
+ display: inline-block;
+ width: 46px;
+}
+
+.log-head {
+ border-radius: 3px;
+ padding: 2px 5px;
+}
+
+.log-exception {
+ line-height: 16px;
+ margin-left: 30px;
+ color: crimson;
+}
diff --git a/RobotApp.Client/RobotApp.Client.csproj b/RobotApp.Client/RobotApp.Client.csproj
index 977a576..481cb7b 100644
--- a/RobotApp.Client/RobotApp.Client.csproj
+++ b/RobotApp.Client/RobotApp.Client.csproj
@@ -20,8 +20,4 @@
-
-
-
-
diff --git a/RobotApp/Controllers/LogsManagerController.cs b/RobotApp/Controllers/LogsManagerController.cs
new file mode 100644
index 0000000..ca9741c
--- /dev/null
+++ b/RobotApp/Controllers/LogsManagerController.cs
@@ -0,0 +1,48 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace RobotApp.Controllers;
+
+[Route("api/[controller]")]
+[ApiController]
+[Authorize]
+public class LogsManagerController(Services.Logger Logger) : ControllerBase
+{
+ private readonly string LoggerDirectory = "logs";
+
+ [HttpGet]
+ public async Task> GetLogs([FromQuery(Name = "date")] DateTime date)
+ {
+ string temp = "";
+ try
+ {
+ string fileName = $"{date:yyyy-MM-dd}.log";
+ string path = Path.Combine(LoggerDirectory, fileName);
+ if (!Path.GetFullPath(path).StartsWith(Path.GetFullPath(LoggerDirectory)))
+ {
+ Logger.Warning($"GetLogs: phát hiện đường dẫn không hợp lệ.");
+ return [];
+ }
+
+ if (!System.IO.File.Exists(path))
+ {
+ Logger.Warning($"GetLogs: không tìm thấy file log của ngày {date.ToShortDateString()} - {path}.");
+ return [];
+ }
+
+ temp = Path.Combine(LoggerDirectory, $"{Guid.NewGuid()}.log");
+ System.IO.File.Copy(path, temp);
+
+ return await System.IO.File.ReadAllLinesAsync(temp);
+ }
+ catch (Exception ex)
+ {
+ Logger.Warning($"GetLogs: Hệ thống có lỗi xảy ra - {ex.Message}");
+ return [];
+ }
+ finally
+ {
+ if (System.IO.File.Exists(temp)) System.IO.File.Delete(temp);
+ }
+ }
+}
diff --git a/RobotApp/Properties/launchSettings.json b/RobotApp/Properties/launchSettings.json
index 21714dc..3f43f90 100644
--- a/RobotApp/Properties/launchSettings.json
+++ b/RobotApp/Properties/launchSettings.json
@@ -17,7 +17,7 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"workingDirectory": "$(TargetDir)",
- "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
+ //"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://0.0.0.0:7150;http://localhost:5229",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
diff --git a/RobotApp/Services/MQTTClient.cs b/RobotApp/Services/MQTTClient.cs
index e21daa6..1ab8acd 100644
--- a/RobotApp/Services/MQTTClient.cs
+++ b/RobotApp/Services/MQTTClient.cs
@@ -24,8 +24,8 @@ public class MQTTClient : IAsyncDisposable
public event Action? InstanceActionsChanged;
public bool IsConnected => !IsDisposed && MqttClient is not null && MqttClient.IsConnected;
- private string OrderTopic => $"{VDA5050Setting.TopicPrefix}/{ClientId}/{VDA5050Topic.ORDER.ToTopicString()}";
- private string InstanceActionsTopic => $"{VDA5050Setting.TopicPrefix}/{ClientId}/{VDA5050Topic.INSTANTACTIONS.ToTopicString()}";
+ private string OrderTopic => $"{VDA5050Setting.TopicPrefix}/{VDA5050Setting.Manufacturer}/{ClientId}/{VDA5050Topic.ORDER.ToTopicString()}";
+ private string InstanceActionsTopic => $"{VDA5050Setting.TopicPrefix}/{VDA5050Setting.Manufacturer}/{ClientId}/{VDA5050Topic.INSTANTACTIONS.ToTopicString()}";
public MQTTClient(string clientId, VDA5050Setting setting, Logger logger)
{
diff --git a/RobotApp/Services/Robot/RobotConnection.cs b/RobotApp/Services/Robot/RobotConnection.cs
index dc35a47..cf3cbed 100644
--- a/RobotApp/Services/Robot/RobotConnection.cs
+++ b/RobotApp/Services/Robot/RobotConnection.cs
@@ -22,7 +22,7 @@ public class RobotConnection(RobotConfiguration RobotConfiguration,
{
try
{
- Logger.Debug($"Nhận Order: {data}");
+ //Logger.Debug($"Nhận Order: {data}");
var msg = JsonSerializer.Deserialize(data, JsonOptionExtends.Read);
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber) return;
OrderUpdated?.Invoke(msg);
@@ -37,7 +37,7 @@ public class RobotConnection(RobotConfiguration RobotConfiguration,
{
try
{
- Logger.Debug($"Nhận InstanceActions: {data}");
+ //Logger.Debug($"Nhận InstanceActions: {data}");
var msg = JsonSerializer.Deserialize(data, JsonOptionExtends.Read);
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber) return;
ActionUpdated?.Invoke(msg);
@@ -50,7 +50,7 @@ public class RobotConnection(RobotConfiguration RobotConfiguration,
public async Task Publish(string topic, string data)
{
- if (MqttClient is not null && MqttClient.IsConnected) return await MqttClient.PublishAsync($"{VDA5050Setting.TopicPrefix}/{RobotConfiguration.SerialNumber}/{topic}", data);
+ if (MqttClient is not null && MqttClient.IsConnected) return await MqttClient.PublishAsync($"{VDA5050Setting.TopicPrefix}/{VDA5050Setting.Manufacturer}/{RobotConfiguration.SerialNumber}/{topic}", data);
return new(false, "Chưa có kết nối tới broker");
}