diff --git a/src/NexusReader.Maui/App.xaml.cs b/src/NexusReader.Maui/App.xaml.cs
index 28db06e..365ccf5 100644
--- a/src/NexusReader.Maui/App.xaml.cs
+++ b/src/NexusReader.Maui/App.xaml.cs
@@ -8,4 +8,22 @@ public partial class App : Microsoft.Maui.Controls.Application
MainPage = new MainPage();
}
+
+ protected override Window CreateWindow(IActivationState? activationState)
+ {
+ var window = base.CreateWindow(activationState);
+
+ // Hook into native MAUI lifecycle events to cleanly flush and close Serilog buffers
+ window.Stopped += (s, e) =>
+ {
+ Serilog.Log.CloseAndFlush();
+ };
+
+ window.Destroying += (s, e) =>
+ {
+ Serilog.Log.CloseAndFlush();
+ };
+
+ return window;
+ }
}
diff --git a/src/NexusReader.Maui/Infrastructure/Logging/BlazorLoggingBridge.cs b/src/NexusReader.Maui/Infrastructure/Logging/BlazorLoggingBridge.cs
new file mode 100644
index 0000000..b667064
--- /dev/null
+++ b/src/NexusReader.Maui/Infrastructure/Logging/BlazorLoggingBridge.cs
@@ -0,0 +1,53 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.JSInterop;
+
+namespace NexusReader.Maui.Infrastructure.Logging;
+
+///
+/// Lightweight bridge service that intercepts logs, console outputs, and uncaught exceptions
+/// from the Blazor WebView/JS side, and routes them directly to Serilog under "BlazorWebView" context.
+///
+public sealed class BlazorLoggingBridge
+{
+ private readonly ILogger _logger;
+
+ public BlazorLoggingBridge(ILoggerFactory loggerFactory)
+ {
+ _logger = loggerFactory.CreateLogger("BlazorWebView");
+ }
+
+ [JSInvokable("LogJsMessage")]
+ public void LogJsMessage(string level, string message, string? stackTrace = null)
+ {
+ if (string.IsNullOrWhiteSpace(message))
+ {
+ return;
+ }
+
+ switch (level.ToLowerInvariant())
+ {
+ case "error":
+ case "exception":
+ if (!string.IsNullOrWhiteSpace(stackTrace))
+ {
+ _logger.LogError("JS Unhandled Exception: {Message}\nStack Trace:\n{StackTrace}", message, stackTrace);
+ }
+ else
+ {
+ _logger.LogError("JS Error: {Message}", message);
+ }
+ break;
+
+ case "warning":
+ case "warn":
+ _logger.LogWarning("JS Warning: {Message}", message);
+ break;
+
+ case "info":
+ case "log":
+ default:
+ _logger.LogInformation("JS Log: {Message}", message);
+ break;
+ }
+ }
+}
diff --git a/src/NexusReader.Maui/Infrastructure/Logging/SerilogConfiguration.cs b/src/NexusReader.Maui/Infrastructure/Logging/SerilogConfiguration.cs
new file mode 100644
index 0000000..7a94779
--- /dev/null
+++ b/src/NexusReader.Maui/Infrastructure/Logging/SerilogConfiguration.cs
@@ -0,0 +1,106 @@
+using Microsoft.Extensions.Logging;
+using Serilog;
+using Serilog.Core;
+using Serilog.Events;
+using Serilog.Formatting;
+using Serilog.Formatting.Display;
+
+namespace NexusReader.Maui.Infrastructure.Logging;
+
+public static class SerilogConfiguration
+{
+ private const string OutputTemplate =
+ "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [ThreadId: {ThreadId}] [{SourceContext}] {Message:lj}{NewLine}{Exception}";
+
+ public static MauiAppBuilder RegisterLogging(this MauiAppBuilder builder)
+ {
+ // 1. Ensure logs directory exists in secure sandbox
+ var logDir = Path.Combine(Microsoft.Maui.Storage.FileSystem.AppDataDirectory, "logs");
+ if (!Directory.Exists(logDir))
+ {
+ Directory.CreateDirectory(logDir);
+ }
+ var logPath = Path.Combine(logDir, "log-.txt");
+
+ // 2. Inject sandboxed log path dynamically into configuration provider
+ builder.Configuration["Serilog:WriteTo:0:Args:configure:0:Args:path"] = logPath;
+
+ // 3. Configure Serilog Logger Configuration using App Configuration settings
+ var loggerConfig = new LoggerConfiguration()
+ .ReadFrom.Configuration(builder.Configuration)
+ .Enrich.With(new ThreadIdEnricher());
+
+ // 4. Platform-specific and environment-specific sinks
+#if ANDROID
+ // Direct Native Android Logcat Sink (JNI bindings for native diagnostics)
+ loggerConfig.WriteTo.Sink(
+ new AndroidLogcatSink(new MessageTemplateTextFormatter(OutputTemplate, null)),
+ restrictedToMinimumLevel: LogEventLevel.Debug);
+#endif
+
+ // 5. Initialize the static Serilog Log
+ Log.Logger = loggerConfig.CreateLogger();
+
+ // 6. Connect Serilog to Microsoft.Extensions.Logging
+ builder.Logging.ClearProviders();
+ builder.Logging.AddSerilog(dispose: true);
+
+ return builder;
+ }
+}
+
+///
+/// A custom self-contained thread enricher to avoid unnecessary NuGet packages.
+///
+internal sealed class ThreadIdEnricher : ILogEventEnricher
+{
+ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
+ {
+ logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadId", Environment.CurrentManagedThreadId));
+ }
+}
+
+#if ANDROID
+///
+/// A high-performance, direct Android Logcat Sink utilizing native Android APIs.
+///
+internal sealed class AndroidLogcatSink : ILogEventSink
+{
+ private readonly ITextFormatter _formatter;
+ private const string Tag = "NexusReader";
+
+ public AndroidLogcatSink(ITextFormatter formatter)
+ {
+ _formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
+ }
+
+ public void Emit(LogEvent logEvent)
+ {
+ using var writer = new StringWriter();
+ _formatter.Format(logEvent, writer);
+ var message = writer.ToString().Trim();
+
+ switch (logEvent.Level)
+ {
+ case LogEventLevel.Verbose:
+ Android.Util.Log.Verbose(Tag, message);
+ break;
+ case LogEventLevel.Debug:
+ Android.Util.Log.Debug(Tag, message);
+ break;
+ case LogEventLevel.Information:
+ Android.Util.Log.Info(Tag, message);
+ break;
+ case LogEventLevel.Warning:
+ Android.Util.Log.Warn(Tag, message);
+ break;
+ case LogEventLevel.Error:
+ Android.Util.Log.Error(Tag, message);
+ break;
+ case LogEventLevel.Fatal:
+ Android.Util.Log.Wtf(Tag, message);
+ break;
+ }
+ }
+}
+#endif
diff --git a/src/NexusReader.Maui/Main.razor b/src/NexusReader.Maui/Main.razor
index 29c775f..95b82eb 100644
--- a/src/NexusReader.Maui/Main.razor
+++ b/src/NexusReader.Maui/Main.razor
@@ -1,5 +1,8 @@
@using Microsoft.AspNetCore.Components.Routing
@using NexusReader.UI.Shared
+@using NexusReader.Maui.Infrastructure.Logging
+@inject IJSRuntime JSRuntime
+@inject BlazorLoggingBridge LoggingBridge
@@ -16,3 +19,21 @@
+
+@code {
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender)
+ {
+ try
+ {
+ var dotNetRef = DotNetObjectReference.Create(LoggingBridge);
+ await JSRuntime.InvokeVoidAsync("NexusLogging.initializeBridge", dotNetRef);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[SerilogBridge] Failed to initialize Blazor/JS Bridge: {ex}");
+ }
+ }
+ }
+}
diff --git a/src/NexusReader.Maui/MauiProgram.cs b/src/NexusReader.Maui/MauiProgram.cs
index 8768784..6024cc6 100644
--- a/src/NexusReader.Maui/MauiProgram.cs
+++ b/src/NexusReader.Maui/MauiProgram.cs
@@ -1,9 +1,11 @@
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Configuration;
using NexusReader.Application.Abstractions.Services;
using NexusReader.Infrastructure.Mobile.Services;
using NexusReader.UI.Shared.Services;
using NexusReader.Application;
using MediatR;
+using NexusReader.Maui.Infrastructure.Logging;
namespace NexusReader.Maui;
@@ -14,16 +16,30 @@ public static class MauiProgram
try
{
var builder = MauiApp.CreateBuilder();
+
+ // Load embedded appsettings.json configuration
+ var assembly = typeof(App).Assembly;
+ using (var stream = assembly.GetManifestResourceStream("NexusReader.Maui.appsettings.json"))
+ {
+ if (stream != null)
+ {
+ ((IConfigurationBuilder)builder.Configuration).AddJsonStream(stream);
+ }
+ }
+
builder
- .UseMauiApp();
+ .UseMauiApp()
+ .RegisterLogging();
builder.Services.AddMauiBlazorWebView();
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
- builder.Logging.AddDebug();
#endif
+ // Interception bridge for JS/Blazor WebView logs
+ builder.Services.AddSingleton();
+
// Minimal Infrastructure
builder.Services.AddSingleton();
builder.Services.AddSingleton();
diff --git a/src/NexusReader.Maui/NexusReader.Maui.csproj b/src/NexusReader.Maui/NexusReader.Maui.csproj
index b92e219..32f874c 100644
--- a/src/NexusReader.Maui/NexusReader.Maui.csproj
+++ b/src/NexusReader.Maui/NexusReader.Maui.csproj
@@ -27,6 +27,13 @@
+
+
+
+
+
+
+
@@ -34,4 +41,8 @@
+
+
+
+
diff --git a/src/NexusReader.Maui/appsettings.json b/src/NexusReader.Maui/appsettings.json
new file mode 100644
index 0000000..14e9ab2
--- /dev/null
+++ b/src/NexusReader.Maui/appsettings.json
@@ -0,0 +1,45 @@
+{
+ "Serilog": {
+ "Using": [
+ "Serilog.Sinks.File",
+ "Serilog.Sinks.Debug",
+ "Serilog.Sinks.Async"
+ ],
+ "MinimumLevel": {
+ "Default": "Debug",
+ "Override": {
+ "Microsoft": "Warning",
+ "Microsoft.AspNetCore": "Warning",
+ "System": "Warning"
+ }
+ },
+ "WriteTo": [
+ {
+ "Name": "Async",
+ "Args": {
+ "configure": [
+ {
+ "Name": "File",
+ "Args": {
+ "path": "LOG_PATH_PLACEHOLDER",
+ "rollingInterval": "Day",
+ "retainedFileCountLimit": 7,
+ "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [ThreadId: {ThreadId}] [{SourceContext}] {Message:lj}{NewLine}{Exception}",
+ "shared": true
+ }
+ }
+ ]
+ }
+ },
+ {
+ "Name": "Debug",
+ "Args": {
+ "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [ThreadId: {ThreadId}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"
+ }
+ }
+ ],
+ "Enrich": [
+ "FromLogContext"
+ ]
+ }
+}
diff --git a/src/NexusReader.Maui/wwwroot/index.html b/src/NexusReader.Maui/wwwroot/index.html
index e0a77ce..8570c5a 100644
--- a/src/NexusReader.Maui/wwwroot/index.html
+++ b/src/NexusReader.Maui/wwwroot/index.html
@@ -26,7 +26,53 @@
+