diff --git a/src/Web/AdminPanel/Pages/LogFiles.razor b/src/Web/AdminPanel/Pages/LogFiles.razor index 5904925..2c7541e 100644 --- a/src/Web/AdminPanel/Pages/LogFiles.razor +++ b/src/Web/AdminPanel/Pages/LogFiles.razor @@ -1,57 +1,320 @@ -@page "/logfiles" +@page "/logfiles" @using System.IO @using MUnique.OpenMU.Web.AdminPanel.Properties +@implements IDisposable +@inject IJSRuntime JSRuntime OpenMU: @Resources.LogFiles -
- - - - - - - - - - @foreach (var entry in this._files.OrderByDescending(f => f.LastWriteTime)) - { - - - - - - } - -
@Resources.FileName@Resources.LastUpdate@Resources.Size
- @entry.Name - @entry.LastWriteTime@FormatFileSize(entry.Length)
+ +
+
+
+
+ + + + + + + + + + + @foreach (var entry in this._files.OrderByDescending(f => f.LastWriteTime)) + { + var isSelected = this._selectedFile?.FullName == entry.FullName; + + + + + + + } + +
@Resources.FileName@Resources.LastUpdate@Resources.SizeActions
+ + @entry.LastWriteTime@FormatFileSize(entry.Length) + + + +
+
+
+
+@if (this._selectedFile != null) +{ +
+
+
+ + Log Viewer: + @this._selectedFile.Name + @FormatFileSize(this._selectedFile.Length) +
+
+
+ + +
+ + +
+
+
+
+
+
+ + + @if (!string.IsNullOrEmpty(this._searchText)) + { + + } +
+
+
+ +
+ @if (this._logLines.Count == 0) + { +
No log entries found.
+ } + else + { + var filteredLines = GetFilteredLines(); + @if (filteredLines.Count == 0) + { +
No log entries match your filter.
+ } + else + { + @foreach (var line in filteredLines) + { +
@line
+ } + } + } +
+ +
+
+ Showing @GetFilteredLines().Count of @this._logLines.Count lines (Last 300 lines loaded). +
+ +
+
+
+} + @code { private readonly List _files = new (); + private FileInfo? _selectedFile; + private List _logLines = new (); + private string _searchText = string.Empty; + private bool _liveUpdate = false; + private System.Threading.Timer? _timer; + private bool _shouldScrollToBottom = false; - /// - /// Initializes a new instance of class . - /// - public LogFiles() + /// + protected override void OnInitialized() { - var files = Directory.GetFiles(Path.Combine(Directory.GetCurrentDirectory(), "logs")); - foreach (var filePath in files) + this.RefreshFileList(); + } + + private void RefreshFileList() + { + this._files.Clear(); + var logsPath = Path.Combine(Directory.GetCurrentDirectory(), "logs"); + if (Directory.Exists(logsPath)) { - this._files.Add(new FileInfo(filePath)); + var files = Directory.GetFiles(logsPath); + foreach (var filePath in files) + { + this._files.Add(new FileInfo(filePath)); + } } } + private void SelectFile(FileInfo file) + { + this._selectedFile = file; + this._searchText = string.Empty; + this.RefreshLogLines(); + this._shouldScrollToBottom = true; + this.SetupTimer(); + } + + private void CloseViewer() + { + this._selectedFile = null; + this._searchText = string.Empty; + this._logLines.Clear(); + this._liveUpdate = false; + this.SetupTimer(); + } + + private void ClearSearch() + { + this._searchText = string.Empty; + } + + private void ToggleLiveUpdate(ChangeEventArgs e) + { + this._liveUpdate = (bool)(e.Value ?? false); + this.SetupTimer(); + } + + private void SetupTimer() + { + if (this._liveUpdate && this._selectedFile != null) + { + this._timer ??= new System.Threading.Timer(_ => + { + InvokeAsync(() => + { + this.RefreshLogLines(); + this.StateHasChanged(); + }); + }, null, 0, 2000); + } + else + { + this._timer?.Dispose(); + this._timer = null; + } + } + + private void RefreshLogLines() + { + if (this._selectedFile == null) + { + return; + } + + this._selectedFile = new FileInfo(this._selectedFile.FullName); + this._logLines = this.ReadLastLines(this._selectedFile.FullName, 300); + } + + private List ReadLastLines(string path, int maxLines) + { + var lines = new List(); + try + { + using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + long offset = Math.Max(0, fs.Length - 102400); // Read last 100 KB + fs.Seek(offset, SeekOrigin.Begin); + using var reader = new StreamReader(fs, System.Text.Encoding.UTF8); + + if (offset > 0) + { + // Discard partial line + reader.ReadLine(); + } + + string? line; + while ((line = reader.ReadLine()) != null) + { + lines.Add(line); + } + + if (lines.Count > maxLines) + { + lines = lines.Skip(lines.Count - maxLines).ToList(); + } + } + catch (Exception ex) + { + lines.Add($"Error reading log file: {ex.Message}"); + } + + return lines; + } + + private List GetFilteredLines() + { + if (string.IsNullOrWhiteSpace(this._searchText)) + { + return this._logLines; + } + + return this._logLines + .Where(line => line.Contains(this._searchText, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + private string GetLineColorStyle(string line) + { + if (line.Contains("[Error]", StringComparison.OrdinalIgnoreCase) || line.Contains("[Critical]", StringComparison.OrdinalIgnoreCase)) + { + return "color: #ff6b6b; font-weight: bold;"; + } + + if (line.Contains("[Warning]", StringComparison.OrdinalIgnoreCase)) + { + return "color: #feca57;"; + } + + if (line.Contains("[Debug]", StringComparison.OrdinalIgnoreCase)) + { + return "color: #8a8d93; font-style: italic;"; + } + + if (line.Contains("[Information]", StringComparison.OrdinalIgnoreCase)) + { + return "color: #1dd1a1;"; + } + + return "color: #d1d2d6;"; + } + + private async Task ScrollToBottom() + { + try + { + await JSRuntime.InvokeVoidAsync("eval", "var el = document.getElementById('log-terminal'); if (el) { el.scrollTop = el.scrollHeight; }"); + } + catch + { + // Ignore error + } + } + + /// + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (this._shouldScrollToBottom) + { + this._shouldScrollToBottom = false; + await this.ScrollToBottom(); + } + } + + /// + public void Dispose() + { + this._timer?.Dispose(); + } + private string FormatFileSize(long size) { return size switch { - (< 1024 << 10) => $"{Math.Round(size / 1024D, 2)} KiB", + (< 1024 << 10) => $"{Math.Round(size / 1024D, 2)} KiB", (< 1024 << 20) => $"{Math.Round(size * 1D / (1024 << 10), 2)} MiB", (< 1024L << 30) => $"{Math.Round(size * 1D / (1024L << 20), 2)} GiB", _ => $"{size} bytes" - }; + }; } }