feat(search/rag): implement NexusSearchBox, dynamic Qdrant collection auto-provisioning, batch vector ingestion, mobile Serilog logging, and resolve 401 auth handler error #51

Merged
mjasin merged 10 commits from feat/nexus-search-box into develop 2026-05-26 12:15:29 +00:00
10 changed files with 738 additions and 5 deletions
Showing only changes of commit f902073bcb - Show all commits
+18
View File
@@ -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;
}
}
@@ -0,0 +1,53 @@
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
namespace NexusReader.Maui.Infrastructure.Logging;
/// <summary>
/// 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.
/// </summary>
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;
}
}
}
@@ -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;
}
}
/// <summary>
/// A custom self-contained thread enricher to avoid unnecessary NuGet packages.
/// </summary>
internal sealed class ThreadIdEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadId", Environment.CurrentManagedThreadId));
}
}
#if ANDROID
/// <summary>
/// A high-performance, direct Android Logcat Sink utilizing native Android APIs.
/// </summary>
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
+21
View File
@@ -1,5 +1,8 @@
@using Microsoft.AspNetCore.Components.Routing
@using NexusReader.UI.Shared
@using NexusReader.Maui.Infrastructure.Logging
@inject IJSRuntime JSRuntime
@inject BlazorLoggingBridge LoggingBridge
<Router AppAssembly="@typeof(NexusReader.UI.Shared._Imports).Assembly">
<Found Context="routeData">
@@ -16,3 +19,21 @@
</LayoutView>
</NotFound>
</Router>
@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}");
}
}
}
}
+18 -2
View File
@@ -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<App>();
.UseMauiApp<App>()
.RegisterLogging();
builder.Services.AddMauiBlazorWebView();
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
builder.Logging.AddDebug();
#endif
// Interception bridge for JS/Blazor WebView logs
builder.Services.AddSingleton<BlazorLoggingBridge>();
// Minimal Infrastructure
builder.Services.AddSingleton<IPlatformService, MauiPlatformService>();
builder.Services.AddSingleton<INativeStorageService, MauiStorageService>();
@@ -27,6 +27,13 @@
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="10.0.20" />
<PackageReference Include="Microsoft.Maui.Essentials" Version="10.0.20" />
<PackageReference Include="Serilog" Version="4.1.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
@@ -34,4 +41,8 @@
<ProjectReference Include="..\NexusReader.Infrastructure.Mobile\NexusReader.Infrastructure.Mobile.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="appsettings.json" />
</ItemGroup>
</Project>
+45
View File
@@ -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"
]
}
}
+46
View File
@@ -26,7 +26,53 @@
<script src="_framework/blazor.webview.js" autostart="false"></script>
<script src="_content/NexusReader.UI.Shared/js/d3.v7.min.js"></script>
<script>
window.NexusLogging = {
initializeBridge: function (dotNetHelper) {
const originalLog = console.log;
const originalWarn = console.warn;
const originalError = console.error;
console.log = function (...args) {
originalLog.apply(console, args);
try {
dotNetHelper.invokeMethodAsync('LogJsMessage', 'info', args.map(x => typeof x === 'object' ? JSON.stringify(x) : String(x)).join(' '));
} catch (e) {}
};
console.warn = function (...args) {
originalWarn.apply(console, args);
try {
dotNetHelper.invokeMethodAsync('LogJsMessage', 'warn', args.map(x => typeof x === 'object' ? JSON.stringify(x) : String(x)).join(' '));
} catch (e) {}
};
console.error = function (...args) {
originalError.apply(console, args);
try {
dotNetHelper.invokeMethodAsync('LogJsMessage', 'error', args.map(x => typeof x === 'object' ? JSON.stringify(x) : String(x)).join(' '));
} catch (e) {}
};
window.onerror = function (message, source, lineno, colno, error) {
const stack = error ? error.stack : '';
try {
dotNetHelper.invokeMethodAsync('LogJsMessage', 'error', `${message} at ${source}:${lineno}:${colno}`, stack);
} catch (e) {}
return false;
};
window.addEventListener('unhandledrejection', function (event) {
const reason = event.reason;
const message = reason instanceof Error ? reason.message : String(reason);
const stack = reason instanceof Error ? reason.stack : '';
try {
dotNetHelper.invokeMethodAsync('LogJsMessage', 'error', `Unhandled Promise Rejection: ${message}`, stack);
} catch (e) {}
});
}
};
</script>
</body>
</html>
@@ -0,0 +1,370 @@
@page "/serilog-demo"
@inject ILogger<SerilogDemo> Logger
@inject IJSRuntime JSRuntime
<div class="serilog-demo-container">
<div class="header-card">
<div class="header-content">
<NexusIcon Name="cpu" Size="36" Class="header-icon" />
<div class="header-text">
<h1>Serilog Logging Infrastructure</h1>
<p class="subtitle">Production-grade diagnostic pipeline for unified native & web logs</p>
</div>
</div>
<div class="status-badge">
<span class="status-dot green"></span>
<span class="status-text">Pipeline Active</span>
</div>
</div>
<div class="demo-grid">
<!-- Native .NET Logging Panel -->
<div class="control-card">
<div class="card-header">
<NexusIcon Name="terminal" Size="20" Class="card-icon" />
<h2>Native .NET Logs (C#)</h2>
</div>
<p class="card-desc">Trigger structured C# logs using Dependency Injected ILogger.</p>
<div class="btn-group">
<button class="btn btn-info" @onclick="LogInfo">
<NexusIcon Name="info" Size="16" />
Log Info
</button>
<button class="btn btn-warning" @onclick="LogWarning">
<NexusIcon Name="alert-triangle" Size="16" />
Log Warning
</button>
<button class="btn btn-error" @onclick="LogError">
<NexusIcon Name="x-circle" Size="16" />
Log Error Exception
</button>
</div>
</div>
<!-- Blazor / JS Interop Bridge Panel -->
<div class="control-card">
<div class="card-header">
<NexusIcon Name="globe" Size="20" Class="card-icon js-icon" />
<h2>Blazor / JS WebView Logs</h2>
</div>
<p class="card-desc">Trigger logs from JavaScript to verify the interop error capture bridge.</p>
<div class="btn-group">
<button class="btn btn-js-info" @onclick="TriggerJsLog">
<NexusIcon Name="message-square" Size="16" />
Trigger console.log()
</button>
<button class="btn btn-js-error" @onclick="TriggerJsException">
<NexusIcon Name="zap" Size="16" />
Trigger JS Exception
</button>
</div>
</div>
</div>
<!-- Active Log Config Panel -->
<div class="config-card">
<div class="card-header">
<NexusIcon Name="settings" Size="20" Class="card-icon" />
<h2>Pipeline Diagnostics</h2>
</div>
<div class="config-grid">
<div class="config-item">
<span class="label">Rolling Daily File Sandbox Path</span>
<span class="value code-value">AppDataDirectory/logs/log-*.txt</span>
</div>
<div class="config-item">
<span class="label">Active Configuration Provider</span>
<span class="value">Serilog.Settings.Configuration (appsettings.json)</span>
</div>
<div class="config-item">
<span class="label">Native Apple Console Sink</span>
<span class="value">Serilog.Sinks.Debug (conditional compilation)</span>
</div>
<div class="config-item">
<span class="label">Native Android Logcat Sink</span>
<span class="value">AndroidLogcatSink (direct JNI bindings)</span>
</div>
</div>
</div>
</div>
<style>
.serilog-demo-container {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
color: #e2e8f0;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
.header-card {
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, rgba(30, 41, 59, 0.7) 0%, rgba(15, 23, 42, 0.8) 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 2rem;
margin-bottom: 2rem;
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
.header-content {
display: flex;
align-items: center;
gap: 1.5rem;
}
.header-icon {
color: #6366f1;
filter: drop-shadow(0 0 8px rgba(99, 102, 241, 0.5));
}
.header-text h1 {
font-size: 1.8rem;
font-weight: 700;
margin: 0;
background: linear-gradient(to right, #ffffff, #94a3b8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
margin: 0.25rem 0 0 0;
color: #94a3b8;
font-size: 0.95rem;
}
.status-badge {
display: flex;
align-items: center;
gap: 0.5rem;
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
padding: 0.5rem 1rem;
border-radius: 9999px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-dot.green {
background-color: #10b981;
box-shadow: 0 0 8px #10b981;
}
.status-text {
font-size: 0.85rem;
font-weight: 600;
color: #10b981;
}
.demo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
@@media (max-width: 768px) {
.demo-grid {
grid-template-columns: 1fr;
}
}
.control-card {
background: rgba(30, 41, 59, 0.45);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(8px);
}
.card-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.card-icon {
color: #6366f1;
}
.js-icon {
color: #eab308;
}
.card-header h2 {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
}
.card-desc {
color: #94a3b8;
font-size: 0.9rem;
margin-bottom: 1.5rem;
line-height: 1.5;
}
.btn-group {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.25rem;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.btn-info {
background-color: rgba(99, 102, 241, 0.1);
color: #818cf8;
border: 1px solid rgba(99, 102, 241, 0.2);
}
.btn-info:hover {
background-color: #6366f1;
color: white;
}
.btn-warning {
background-color: rgba(245, 158, 11, 0.1);
color: #fbbf24;
border: 1px solid rgba(245, 158, 11, 0.2);
}
.btn-warning:hover {
background-color: #f59e0b;
color: white;
}
.btn-error {
background-color: rgba(239, 68, 68, 0.1);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.2);
}
.btn-error:hover {
background-color: #ef4444;
color: white;
}
.btn-js-info {
background-color: rgba(234, 179, 8, 0.1);
color: #fef08a;
border: 1px solid rgba(234, 179, 8, 0.2);
}
.btn-js-info:hover {
background-color: #eab308;
color: #0f172a;
}
.btn-js-error {
background-color: rgba(236, 72, 153, 0.1);
color: #fbcfe8;
border: 1px solid rgba(236, 72, 153, 0.2);
}
.btn-js-error:hover {
background-color: #ec4899;
color: white;
}
.config-card {
background: rgba(15, 23, 42, 0.5);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 16px;
padding: 2rem;
}
.config-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-top: 1.5rem;
}
@@media (max-width: 768px) {
.config-grid {
grid-template-columns: 1fr;
}
}
.config-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.config-item .label {
font-size: 0.8rem;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
}
.config-item .value {
font-size: 0.95rem;
color: #cbd5e1;
}
.code-value {
font-family: 'Fira Code', 'Courier New', Courier, monospace;
background: rgba(0, 0, 0, 0.2);
padding: 0.25rem 0.5rem;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.05);
word-break: break-all;
}
</style>
@code {
private void LogInfo()
{
Logger.LogInformation("Structured native log triggered by user from SerilogDemo. Button: LogInfo");
}
private void LogWarning()
{
Logger.LogWarning("Potential warning log triggered from Blazor razor component at {Time}", DateTime.UtcNow);
}
private void LogError()
{
try
{
throw new InvalidOperationException("Simulated native C# operation exception triggered in Diagnostic dashboard.");
}
catch (Exception ex)
{
Logger.LogError(ex, "Captured exception successfully in native Serilog pipeline!");
}
}
private async Task TriggerJsLog()
{
await JSRuntime.InvokeVoidAsync("console.log", "Intercepted JS console statement from Blazor WebView interop trigger!");
}
private async Task TriggerJsException()
{
await JSRuntime.InvokeVoidAsync("eval", "throw new Error('Simulated runtime JS Exception triggered from Blazor UI button click!');");
}
}
+50 -3
View File
@@ -2,16 +2,63 @@
@attribute [Authorize]
<div class="settings-page">
<h1>Ustawienia</h1>
<p>Konfiguracja Twojego konta i preferencji czytania.</p>
<h1>Settings</h1>
<p>Configure your account and application preferences.</p>
<div class="settings-section">
<h2>Diagnostics & System Logs</h2>
<p>Inspect native logging infrastructure, trigger custom logs, and trace WebView errors.</p>
<a class="diag-btn" href="/serilog-demo">
<NexusIcon Name="cpu" Size="16" />
Open Serilog Diagnostics Dashboard
</a>
</div>
</div>
<style>
.settings-page {
padding: 2rem;
color: #e2e8f0;
font-family: 'Inter', sans-serif;
}
h1 {
margin-bottom: 1rem;
margin-bottom: 0.5rem;
color: #fff;
}
h2 {
margin-top: 2rem;
font-size: 1.2rem;
color: #fff;
margin-bottom: 0.5rem;
}
.settings-section {
background: rgba(30, 41, 59, 0.45);
border: 1px solid rgba(255, 255, 255, 0.05);
padding: 1.5rem;
border-radius: 12px;
margin-top: 1.5rem;
}
.settings-section p {
color: #94a3b8;
font-size: 0.9rem;
margin-bottom: 1.25rem;
}
.diag-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: rgba(99, 102, 241, 0.1);
color: #818cf8;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 0.75rem 1.25rem;
border-radius: 8px;
text-decoration: none;
font-size: 0.85rem;
font-weight: 600;
transition: all 0.2s ease;
}
.diag-btn:hover {
background: #6366f1;
color: white;
}
</style>