Fix build and behavior issues in the admin panel log viewer
- Resources.resx: close the unterminated DownloadFile data element, which
made the file invalid XML, and remove the duplicated Actions and Refresh
entries which already exist.
- Resources.Designer.cs: restore the UTF-8 BOM and put the new properties
into the alphabetical order the strongly typed resource builder produces,
so the file matches its generated form again.
- LogFiles.razor: import the collocated script from ./Pages/LogFiles.razor.js.
The _content/{PackageId} prefix only applies to razor class libraries, so
the import failed for this web application and the module was never loaded.
- LogFiles.razor: follow the new entries in live mode again by using the
isScrolledToBottom helper, so the terminal scrolls along unless the user
scrolled up to read the history.
- LogFiles.razor: only catch the expected javascript interop exceptions and
log a failing module import instead of swallowing it silently.
- LogFiles.razor: restore the BOM and the trailing newline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vjs6n29WzQx8pGg3KJPGXk
(cherry picked from commit 7be969ea7b2a9b7441a8c5a65cc8a0928009d595)
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
@page "/logfiles"
|
@page "/logfiles"
|
||||||
|
|
||||||
@using System.IO
|
@using System.IO
|
||||||
|
@using Microsoft.Extensions.Logging
|
||||||
@using MUnique.OpenMU.Web.AdminPanel.Properties
|
@using MUnique.OpenMU.Web.AdminPanel.Properties
|
||||||
@implements IAsyncDisposable
|
@implements IAsyncDisposable
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject ILogger<LogFiles> Logger
|
||||||
|
|
||||||
<PageTitle>OpenMU: @Resources.LogFiles</PageTitle>
|
<PageTitle>OpenMU: @Resources.LogFiles</PageTitle>
|
||||||
<Breadcrumb IsFirstFromRoot="true" Caption="@Resources.LogFiles"/>
|
<Breadcrumb IsFirstFromRoot="true" Caption="@Resources.LogFiles"/>
|
||||||
@@ -103,7 +105,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="log-terminal" class="p-3 rounded" style="height: 480px; overflow-y: auto; font-family: 'Consolas', 'Liberation Mono', Menlo, Courier, monospace; font-size: 13px; line-height: 1.5; white-space: pre-wrap; background-color: #0c0c0d !important; border: 1px solid #2d2d30;">
|
<div id="@TerminalElementId" class="p-3 rounded" style="height: 480px; overflow-y: auto; font-family: 'Consolas', 'Liberation Mono', Menlo, Courier, monospace; font-size: 13px; line-height: 1.5; white-space: pre-wrap; background-color: #0c0c0d !important; border: 1px solid #2d2d30;">
|
||||||
@if (this._logLines.Count == 0)
|
@if (this._logLines.Count == 0)
|
||||||
{
|
{
|
||||||
<div class="text-muted text-center py-5">@Resources.NoLogEntriesFound</div>
|
<div class="text-muted text-center py-5">@Resources.NoLogEntriesFound</div>
|
||||||
@@ -139,6 +141,7 @@
|
|||||||
private const int MaxLogLinesToRead = 300;
|
private const int MaxLogLinesToRead = 300;
|
||||||
private const long LogReadBufferSizeBytes = 102400; // 100 KB
|
private const long LogReadBufferSizeBytes = 102400; // 100 KB
|
||||||
private const int LiveUpdateIntervalMs = 2000;
|
private const int LiveUpdateIntervalMs = 2000;
|
||||||
|
private const string TerminalElementId = "log-terminal";
|
||||||
|
|
||||||
private readonly List<FileInfo> _files = new();
|
private readonly List<FileInfo> _files = new();
|
||||||
private FileInfo? _selectedFile;
|
private FileInfo? _selectedFile;
|
||||||
@@ -164,9 +167,9 @@
|
|||||||
{
|
{
|
||||||
await this._jsModule.DisposeAsync();
|
await this._jsModule.DisposeAsync();
|
||||||
}
|
}
|
||||||
catch
|
catch (JSDisconnectedException)
|
||||||
{
|
{
|
||||||
// Ignore JS module disposal errors
|
// The circuit is already gone, so the module is disposed anyway.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,11 +187,16 @@
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
this._jsModule = await this.JSRuntime.InvokeAsync<IJSObjectReference>("import", "./_content/MUnique.OpenMU.Web.AdminPanel/Pages/LogFiles.razor.js");
|
this._jsModule = await this.JSRuntime.InvokeAsync<IJSObjectReference>("import", "./Pages/LogFiles.razor.js");
|
||||||
}
|
}
|
||||||
catch
|
catch (JSException ex)
|
||||||
{
|
{
|
||||||
// Fallback gracefully if JS module import fails
|
// Without the module, the viewer still works - only the automatic scrolling is unavailable.
|
||||||
|
this.Logger.LogWarning(ex, "Could not load the log viewer javascript module.");
|
||||||
|
}
|
||||||
|
catch (JSDisconnectedException)
|
||||||
|
{
|
||||||
|
// The circuit is gone; nothing to do.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,7 +341,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.InvokeAsync(() =>
|
this.InvokeAsync(async () =>
|
||||||
{
|
{
|
||||||
if (this._disposed || this._selectedFile == null)
|
if (this._disposed || this._selectedFile == null)
|
||||||
{
|
{
|
||||||
@@ -341,11 +349,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var updatedInfo = new FileInfo(this._selectedFile.FullName);
|
var updatedInfo = new FileInfo(this._selectedFile.FullName);
|
||||||
if (updatedInfo.Length != this._lastFileLength || updatedInfo.LastWriteTimeUtc != this._lastFileWriteTime)
|
if (updatedInfo.Length == this._lastFileLength && updatedInfo.LastWriteTimeUtc == this._lastFileWriteTime)
|
||||||
{
|
{
|
||||||
this.RefreshLogLines();
|
return;
|
||||||
this.StateHasChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only follow the new entries when the user didn't scroll up to read the history.
|
||||||
|
var isFollowing = await this.IsScrolledToBottomAsync();
|
||||||
|
this.RefreshLogLines();
|
||||||
|
this._shouldScrollToBottom = isFollowing;
|
||||||
|
this.StateHasChanged();
|
||||||
});
|
});
|
||||||
}, null, 0, LiveUpdateIntervalMs);
|
}, null, 0, LiveUpdateIntervalMs);
|
||||||
}
|
}
|
||||||
@@ -387,16 +400,35 @@
|
|||||||
|
|
||||||
private async Task ScrollToBottomAsync()
|
private async Task ScrollToBottomAsync()
|
||||||
{
|
{
|
||||||
if (this._jsModule != null)
|
if (this._jsModule is null)
|
||||||
{
|
{
|
||||||
try
|
return;
|
||||||
{
|
}
|
||||||
await this._jsModule.InvokeVoidAsync("scrollToBottom", "log-terminal");
|
|
||||||
}
|
try
|
||||||
catch
|
{
|
||||||
{
|
await this._jsModule.InvokeVoidAsync("scrollToBottom", TerminalElementId);
|
||||||
// Ignore JS call errors
|
}
|
||||||
}
|
catch (JSDisconnectedException)
|
||||||
|
{
|
||||||
|
// The circuit is gone; nothing to do.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private async ValueTask<bool> IsScrolledToBottomAsync()
|
||||||
|
{
|
||||||
|
if (this._jsModule is null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await this._jsModule.InvokeAsync<bool>("isScrolledToBottom", TerminalElementId);
|
||||||
|
}
|
||||||
|
catch (JSDisconnectedException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
182
src/Web/AdminPanel/Properties/Resources.Designer.cs
generated
182
src/Web/AdminPanel/Properties/Resources.Designer.cs
generated
@@ -1,4 +1,4 @@
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// <auto-generated>
|
// <auto-generated>
|
||||||
// This code was generated by a tool.
|
// This code was generated by a tool.
|
||||||
// Runtime Version:4.0.30319.42000
|
// Runtime Version:4.0.30319.42000
|
||||||
@@ -249,6 +249,15 @@ namespace MUnique.OpenMU.Web.AdminPanel.Properties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Close.
|
||||||
|
/// </summary>
|
||||||
|
public static string Close {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Close", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Command.
|
/// Looks up a localized string similar to Command.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -468,6 +477,15 @@ namespace MUnique.OpenMU.Web.AdminPanel.Properties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Download File.
|
||||||
|
/// </summary>
|
||||||
|
public static string DownloadFile {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DownloadFile", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Drop item groups.
|
/// Looks up a localized string similar to Drop item groups.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -558,6 +576,15 @@ namespace MUnique.OpenMU.Web.AdminPanel.Properties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Filter log entries....
|
||||||
|
/// </summary>
|
||||||
|
public static string FilterLogEntries {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("FilterLogEntries", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Finished! Have fun :).
|
/// Looks up a localized string similar to Finished! Have fun :).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -774,6 +801,15 @@ namespace MUnique.OpenMU.Web.AdminPanel.Properties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Live.
|
||||||
|
/// </summary>
|
||||||
|
public static string Live {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Live", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Live Map.
|
/// Looks up a localized string similar to Live Map.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -819,6 +855,15 @@ namespace MUnique.OpenMU.Web.AdminPanel.Properties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Log Viewer.
|
||||||
|
/// </summary>
|
||||||
|
public static string LogViewer {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("LogViewer", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Major.
|
/// Looks up a localized string similar to Major.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -972,6 +1017,24 @@ namespace MUnique.OpenMU.Web.AdminPanel.Properties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to No log entries found..
|
||||||
|
/// </summary>
|
||||||
|
public static string NoLogEntriesFound {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("NoLogEntriesFound", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to No log entries match your filter..
|
||||||
|
/// </summary>
|
||||||
|
public static string NoLogEntriesMatchFilter {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("NoLogEntriesMatchFilter", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to This command has no parameters..
|
/// Looks up a localized string similar to This command has no parameters..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1197,6 +1260,15 @@ namespace MUnique.OpenMU.Web.AdminPanel.Properties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Reload File List.
|
||||||
|
/// </summary>
|
||||||
|
public static string ReloadFileList {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ReloadFileList", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Remove.
|
/// Looks up a localized string similar to Remove.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1260,6 +1332,15 @@ namespace MUnique.OpenMU.Web.AdminPanel.Properties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Scroll to Bottom.
|
||||||
|
/// </summary>
|
||||||
|
public static string ScrollToBottom {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ScrollToBottom", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Search.
|
/// Looks up a localized string similar to Search.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1377,6 +1458,15 @@ namespace MUnique.OpenMU.Web.AdminPanel.Properties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Showing {0} of {1} lines (Last {2} lines loaded)..
|
||||||
|
/// </summary>
|
||||||
|
public static string ShowingXOfYLines {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ShowingXOfYLines", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Size.
|
/// Looks up a localized string similar to Size.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1646,95 +1736,5 @@ namespace MUnique.OpenMU.Web.AdminPanel.Properties {
|
|||||||
return ResourceManager.GetString("YesCreateTestAccounts", resourceCulture);
|
return ResourceManager.GetString("YesCreateTestAccounts", resourceCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Log Viewer.
|
|
||||||
/// </summary>
|
|
||||||
public static string LogViewer {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("LogViewer", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Live.
|
|
||||||
/// </summary>
|
|
||||||
public static string Live {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("Live", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Close.
|
|
||||||
/// </summary>
|
|
||||||
public static string Close {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("Close", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Filter log entries....
|
|
||||||
/// </summary>
|
|
||||||
public static string FilterLogEntries {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("FilterLogEntries", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to No log entries found..
|
|
||||||
/// </summary>
|
|
||||||
public static string NoLogEntriesFound {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("NoLogEntriesFound", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to No log entries match your filter..
|
|
||||||
/// </summary>
|
|
||||||
public static string NoLogEntriesMatchFilter {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("NoLogEntriesMatchFilter", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Showing {0} of {1} lines (Last {2} lines loaded)..
|
|
||||||
/// </summary>
|
|
||||||
public static string ShowingXOfYLines {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ShowingXOfYLines", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Scroll to Bottom.
|
|
||||||
/// </summary>
|
|
||||||
public static string ScrollToBottom {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ScrollToBottom", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Reload File List.
|
|
||||||
/// </summary>
|
|
||||||
public static string ReloadFileList {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ReloadFileList", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Download File.
|
|
||||||
/// </summary>
|
|
||||||
public static string DownloadFile {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("DownloadFile", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<root>
|
<root>
|
||||||
<!--
|
<!--
|
||||||
Microsoft ResX Schema
|
Microsoft ResX Schema
|
||||||
@@ -612,18 +612,12 @@
|
|||||||
<data name="Target" xml:space="preserve">
|
<data name="Target" xml:space="preserve">
|
||||||
<value>Target</value>
|
<value>Target</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Actions" xml:space="preserve">
|
|
||||||
<value>Actions</value>
|
|
||||||
</data>
|
|
||||||
<data name="LogViewer" xml:space="preserve">
|
<data name="LogViewer" xml:space="preserve">
|
||||||
<value>Log Viewer</value>
|
<value>Log Viewer</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Live" xml:space="preserve">
|
<data name="Live" xml:space="preserve">
|
||||||
<value>Live</value>
|
<value>Live</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Refresh" xml:space="preserve">
|
|
||||||
<value>Refresh</value>
|
|
||||||
</data>
|
|
||||||
<data name="Close" xml:space="preserve">
|
<data name="Close" xml:space="preserve">
|
||||||
<value>Close</value>
|
<value>Close</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
Reference in New Issue
Block a user