From f5b4292af13935735b09e0b08c2c0de26c3873c8 Mon Sep 17 00:00:00 2001 From: Rhefew Date: Mon, 27 Jul 2026 11:54:47 +0200 Subject: [PATCH] fix(admin): address code review feedback on log viewer localization, CSP JS module, timer disposal, and scroll UX --- src/Web/AdminPanel/Pages/LogFiles.razor | 152 ++++++++++++------ src/Web/AdminPanel/Pages/LogFiles.razor.js | 14 ++ .../Properties/Resources.Designer.cs | 92 ++++++++++- src/Web/AdminPanel/Properties/Resources.resx | 36 +++++ 4 files changed, 247 insertions(+), 47 deletions(-) create mode 100644 src/Web/AdminPanel/Pages/LogFiles.razor.js diff --git a/src/Web/AdminPanel/Pages/LogFiles.razor b/src/Web/AdminPanel/Pages/LogFiles.razor index 2693e25..c79ef4b 100644 --- a/src/Web/AdminPanel/Pages/LogFiles.razor +++ b/src/Web/AdminPanel/Pages/LogFiles.razor @@ -1,8 +1,8 @@ -@page "/logfiles" +@page "/logfiles" @using System.IO @using MUnique.OpenMU.Web.AdminPanel.Properties -@implements IDisposable +@implements IAsyncDisposable @inject IJSRuntime JSRuntime OpenMU: @Resources.LogFiles @@ -14,8 +14,8 @@
- Log Files -
@@ -30,7 +30,7 @@ @Resources.LastUpdate @Resources.Size } - Actions + @Resources.Actions @@ -53,7 +53,7 @@ @FormatFileSize(entry.Length) } - + @@ -73,19 +73,19 @@
- Log Viewer: + @Resources.LogViewer: @this._selectedFile.Name
- +
@@ -94,7 +94,7 @@
- + @if (!string.IsNullOrEmpty(this._searchText)) { @@ -106,31 +106,27 @@
@if (this._logLines.Count == 0) { -
No log entries found.
+
@Resources.NoLogEntriesFound
+ } + else if (this._filteredLines.Count == 0) + { +
@Resources.NoLogEntriesMatchFilter
} else { - var filteredLines = this.GetFilteredLines(); - @if (filteredLines.Count == 0) + @foreach (var line in this._filteredLines) { -
No log entries match your filter.
- } - else - { - @foreach (var line in filteredLines) - { -
@line
- } +
@line
} }
- Showing @this.GetFilteredLines().Count of @this._logLines.Count lines (Last 300 lines loaded). + @string.Format(Resources.ShowingXOfYLines, this._filteredLines.Count, this._logLines.Count, MaxLogLinesToRead)
@@ -140,18 +136,39 @@
@code { + private const int MaxLogLinesToRead = 300; + private const long LogReadBufferSizeBytes = 102400; // 100 KB + private const int LiveUpdateIntervalMs = 2000; + private readonly List _files = new(); private FileInfo? _selectedFile; + private long _lastFileLength; + private DateTime _lastFileWriteTime; private List _logLines = new(); + private List _filteredLines = new(); private string _searchText = string.Empty; private bool _liveUpdate; private System.Threading.Timer? _timer; private bool _shouldScrollToBottom; + private bool _disposed; + private IJSObjectReference? _jsModule; /// - public void Dispose() + public async ValueTask DisposeAsync() { + this._disposed = true; this._timer?.Dispose(); + if (this._jsModule != null) + { + try + { + await this._jsModule.DisposeAsync(); + } + catch + { + // Ignore JS module disposal errors + } + } } /// @@ -163,10 +180,22 @@ /// protected override async Task OnAfterRenderAsync(bool firstRender) { + if (firstRender) + { + try + { + this._jsModule = await this.JSRuntime.InvokeAsync("import", "./_content/MUnique.OpenMU.Web.AdminPanel/Pages/LogFiles.razor.js"); + } + catch + { + // Fallback gracefully if JS module import fails + } + } + if (this._shouldScrollToBottom) { this._shouldScrollToBottom = false; - await this.ScrollToBottomAsync().ConfigureAwait(false); + await this.ScrollToBottomAsync(); } } @@ -187,7 +216,7 @@ 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 + long offset = Math.Max(0, fs.Length - LogReadBufferSizeBytes); fs.Seek(offset, SeekOrigin.Begin); using var reader = new StreamReader(fs, System.Text.Encoding.UTF8); @@ -259,6 +288,7 @@ { this._selectedFile = file; this._searchText = string.Empty; + this._lastFileLength = -1; this.RefreshLogLines(); this._shouldScrollToBottom = true; this.SetupTimer(); @@ -269,6 +299,7 @@ this._selectedFile = null; this._searchText = string.Empty; this._logLines.Clear(); + this._filteredLines.Clear(); this._liveUpdate = false; this.SetupTimer(); } @@ -276,6 +307,13 @@ private void ClearSearch() { this._searchText = string.Empty; + this.UpdateFilteredLines(); + } + + private void OnSearchInput(ChangeEventArgs e) + { + this._searchText = e.Value?.ToString() ?? string.Empty; + this.UpdateFilteredLines(); } private void ToggleLiveUpdate(ChangeEventArgs e) @@ -290,12 +328,26 @@ { this._timer ??= new System.Threading.Timer(_ => { + if (this._disposed) + { + return; + } + this.InvokeAsync(() => { - this.RefreshLogLines(); - this.StateHasChanged(); + if (this._disposed || this._selectedFile == null) + { + return; + } + + var updatedInfo = new FileInfo(this._selectedFile.FullName); + if (updatedInfo.Length != this._lastFileLength || updatedInfo.LastWriteTimeUtc != this._lastFileWriteTime) + { + this.RefreshLogLines(); + this.StateHasChanged(); + } }); - }, null, 0, 2000); + }, null, 0, LiveUpdateIntervalMs); } else { @@ -311,32 +363,40 @@ return; } - this._selectedFile = new FileInfo(this._selectedFile.FullName); - this._logLines = ReadLastLines(this._selectedFile.FullName, 300); - this._shouldScrollToBottom = true; + var fileInfo = new FileInfo(this._selectedFile.FullName); + this._selectedFile = fileInfo; + this._lastFileLength = fileInfo.Length; + this._lastFileWriteTime = fileInfo.LastWriteTimeUtc; + this._logLines = ReadLastLines(fileInfo.FullName, MaxLogLinesToRead); + this.UpdateFilteredLines(); } - private List GetFilteredLines() + private void UpdateFilteredLines() { if (string.IsNullOrWhiteSpace(this._searchText)) { - return this._logLines; + this._filteredLines = this._logLines; + } + else + { + this._filteredLines = this._logLines + .Where(line => line.Contains(this._searchText, StringComparison.OrdinalIgnoreCase)) + .ToList(); } - - return this._logLines - .Where(line => line.Contains(this._searchText, StringComparison.OrdinalIgnoreCase)) - .ToList(); } private async Task ScrollToBottomAsync() { - try + if (this._jsModule != null) { - await this.JSRuntime.InvokeVoidAsync("eval", "var el = document.getElementById('log-terminal'); if (el) { el.scrollTop = el.scrollHeight; }").ConfigureAwait(false); - } - catch - { - // Ignore error + try + { + await this._jsModule.InvokeVoidAsync("scrollToBottom", "log-terminal"); + } + catch + { + // Ignore JS call errors + } } } -} +} \ No newline at end of file diff --git a/src/Web/AdminPanel/Pages/LogFiles.razor.js b/src/Web/AdminPanel/Pages/LogFiles.razor.js new file mode 100644 index 0000000..4b2efcd --- /dev/null +++ b/src/Web/AdminPanel/Pages/LogFiles.razor.js @@ -0,0 +1,14 @@ +export function scrollToBottom(elementId) { + const el = document.getElementById(elementId); + if (el) { + el.scrollTop = el.scrollHeight; + } +} + +export function isScrolledToBottom(elementId) { + const el = document.getElementById(elementId); + if (el) { + return Math.abs(el.scrollHeight - el.clientHeight - el.scrollTop) < 50; + } + return true; +} diff --git a/src/Web/AdminPanel/Properties/Resources.Designer.cs b/src/Web/AdminPanel/Properties/Resources.Designer.cs index 0b344f8..0310f80 100644 --- a/src/Web/AdminPanel/Properties/Resources.Designer.cs +++ b/src/Web/AdminPanel/Properties/Resources.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 @@ -1646,5 +1646,95 @@ namespace MUnique.OpenMU.Web.AdminPanel.Properties { return ResourceManager.GetString("YesCreateTestAccounts", resourceCulture); } } + + /// + /// Looks up a localized string similar to Log Viewer. + /// + public static string LogViewer { + get { + return ResourceManager.GetString("LogViewer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Live. + /// + public static string Live { + get { + return ResourceManager.GetString("Live", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Close. + /// + public static string Close { + get { + return ResourceManager.GetString("Close", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Filter log entries.... + /// + public static string FilterLogEntries { + get { + return ResourceManager.GetString("FilterLogEntries", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No log entries found.. + /// + public static string NoLogEntriesFound { + get { + return ResourceManager.GetString("NoLogEntriesFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No log entries match your filter.. + /// + public static string NoLogEntriesMatchFilter { + get { + return ResourceManager.GetString("NoLogEntriesMatchFilter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Showing {0} of {1} lines (Last {2} lines loaded).. + /// + public static string ShowingXOfYLines { + get { + return ResourceManager.GetString("ShowingXOfYLines", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Scroll to Bottom. + /// + public static string ScrollToBottom { + get { + return ResourceManager.GetString("ScrollToBottom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reload File List. + /// + public static string ReloadFileList { + get { + return ResourceManager.GetString("ReloadFileList", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Download File. + /// + public static string DownloadFile { + get { + return ResourceManager.GetString("DownloadFile", resourceCulture); + } + } } } diff --git a/src/Web/AdminPanel/Properties/Resources.resx b/src/Web/AdminPanel/Properties/Resources.resx index 000bb99..50a2350 100644 --- a/src/Web/AdminPanel/Properties/Resources.resx +++ b/src/Web/AdminPanel/Properties/Resources.resx @@ -612,4 +612,40 @@ Target + + Actions + + + Log Viewer + + + Live + + + Refresh + + + Close + + + Filter log entries... + + + No log entries found. + + + No log entries match your filter. + + + Showing {0} of {1} lines (Last {2} lines loaded). + + + Scroll to Bottom + + + Reload File List + + + Download File + \ No newline at end of file