diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index a25e802..0ce687e 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -152,7 +152,17 @@
"PowerShell(dotnet build AyCode.Services\\\\AyCode.Services.csproj --nologo -v q 2>$null)",
"PowerShell(dotnet test AyCode.Services.Server.Tests\\\\AyCode.Services.Server.Tests.csproj --nologo --no-restore --filter \"FullyQualifiedName~AcBinaryHubProtocolConcurrencyTests\" 2>$null)",
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Blazor\" diff --stat HEAD)",
- "Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Blazor\" diff HEAD -- AyCode.Blazor.Components/Components/Grids/MgGridBase.cs)"
+ "Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Blazor\" diff HEAD -- AyCode.Blazor.Components/Components/Grids/MgGridBase.cs)",
+ "Bash(find \"C:/Users/Fullepi/.nuget/packages/devexpress.blazor/25.1.3\" -name \"*.xml\" 2>/dev/null | head -3)",
+ "Bash(ls \"C:/Users/Fullepi/.nuget/packages/\" 2>/dev/null | grep -i devexpress | head -5; find \"C:/Users/Fullepi/.nuget/packages\" -maxdepth 4 -iname \"DevExpress.Blazor*.xml\" 2>/dev/null | head -3)",
+ "Bash(git -C \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin\" rev-parse --show-toplevel)",
+ "Bash(git -C \"H:/Applications/Mango/Source\" rev-parse --show-toplevel)",
+ "Bash(git -C \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins\" log --oneline -5 -- \"Nop.Plugin.Misc.AIPlugin/docs/SIGNALR/README.md\")",
+ "Bash(git -C \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins\" show HEAD:\"Nop.Plugin.Misc.AIPlugin/docs/SIGNALR/README.md\")",
+ "Bash(git -C \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins\" show 13e96ea:\"Nop.Plugin.Misc.AIPlugin/docs/SIGNALR/README.md\")",
+ "Bash(git -C \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins\" restore --source 13e96ea -- \"Nop.Plugin.Misc.AIPlugin/docs/SIGNALR/README.md\")",
+ "Bash(git -C \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins\" status --short)",
+ "Bash(exit 1)"
]
}
}
diff --git a/AyCode.Core/Loggers/AcLoggerOptions.cs b/AyCode.Core/Loggers/AcLoggerOptions.cs
index 153c5f2..c66e0dc 100644
--- a/AyCode.Core/Loggers/AcLoggerOptions.cs
+++ b/AyCode.Core/Loggers/AcLoggerOptions.cs
@@ -20,8 +20,10 @@ namespace AyCode.Core.Loggers;
///
public sealed class AcLoggerOptions
{
- /// Application type stamped on every log entry. Default: .
- public AppType AppType { get; set; } = AppType.Server;
+ /// Application type stamped on every log entry. null = a config nem adja meg — ilyenkor az
+ /// AddAcLoggerFactory(defaultAppType) kód-defaultja érvényesül (host-onként a helyes érték: Web/Mobile/Server).
+ /// Ha az appsettings kimondja, MINDIG a config nyer.
+ public AppType? AppType { get; set; }
/// Global minimum log level. Entries below this level are discarded. Default: .
public LogLevel LogLevel { get; set; } = LogLevel.Info;
diff --git a/AyCode.Core/Loggers/AcLoggerServiceExtensions.cs b/AyCode.Core/Loggers/AcLoggerServiceExtensions.cs
index 5ccf048..8337a23 100644
--- a/AyCode.Core/Loggers/AcLoggerServiceExtensions.cs
+++ b/AyCode.Core/Loggers/AcLoggerServiceExtensions.cs
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
+using AyCode.Core.Enums;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@@ -23,31 +24,39 @@ public static class AcLoggerServiceExtensions
/// Concrete logger type. Must expose a public constructor with signature
/// (AppType, LogLevel, string?, params IAcLogWriterBase[]).
///
+ /// The service collection.
+ /// A host kód-defaultja (pl. WASM/MAUI kliensen ) — CSAK akkor
+ /// érvényesül, ha az appsettings () nem adja meg; ha a config kimondja, az nyer.
public static IServiceCollection AddAcLoggerFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TLogger>(
- this IServiceCollection services)
+ this IServiceCollection services, AppType defaultAppType = AppType.Server)
where TLogger : AcLoggerBase
{
- return services.AddAcLoggerFactory();
+ return services.AddAcLoggerFactory(defaultAppType);
}
///
/// Registers a logger factory with a custom writer marker interface — e.g. a client-only marker
/// to keep server-only and client-only writers separated at the DI level.
///
- /// Concrete logger type. See for ctor requirements.
+ /// Concrete logger type. See for ctor requirements.
/// Writer marker interface; must extend .
+ /// The service collection.
+ /// A host kód-defaultja — csak config-érték hiányában érvényesül (a config nyer).
public static IServiceCollection AddAcLoggerFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TLogger, TWriterBase>(
- this IServiceCollection services)
+ this IServiceCollection services, AppType defaultAppType = AppType.Server)
where TLogger : AcLoggerBase
where TWriterBase : class, IAcLogWriterBase
{
services.AddSingleton>(sp =>
{
var opts = sp.GetRequiredService>().Value;
+ // Prioritás: appsettings (ha megadva) → a hívó defaultAppType-ja. Így a host kódja hordozza a helyes
+ // alapértéket (kliensen Web/Mobile), a config pedig runtime felülírást ad újrafordítás nélkül.
+ var appType = opts.AppType ?? defaultAppType;
return categoryName =>
{
var writers = sp.GetServices().Cast().ToArray();
- return (TLogger)Activator.CreateInstance(typeof(TLogger), opts.AppType, opts.LogLevel, categoryName, writers)!;
+ return (TLogger)Activator.CreateInstance(typeof(TLogger), appType, opts.LogLevel, categoryName, writers)!;
};
});
diff --git a/AyCode.Core/docs/LOGGING/README.md b/AyCode.Core/docs/LOGGING/README.md
index 254272d..d782ca6 100644
--- a/AyCode.Core/docs/LOGGING/README.md
+++ b/AyCode.Core/docs/LOGGING/README.md
@@ -125,10 +125,12 @@ services.AddSingleton();
// Client-scoped marker variant:
// services.AddSingleton();
-// 3. Register the logger factory (Func singleton in DI)
-services.AddAcLoggerFactory();
+// 3. Register the logger factory (Func singleton in DI).
+// The parameter is the HOST's default AppType (e.g. Web on a WASM/MAUI client) — applied only
+// when appsettings does NOT specify AyCode:Logger:AppType; a config value always wins.
+services.AddAcLoggerFactory(AppType.Web);
// Custom writer-marker overload — pulls only TWriterBase-registered writers:
-// services.AddAcLoggerFactory();
+// services.AddAcLoggerFactory(AppType.Web);
```
Consumers inject `Func` and invoke it with a category name.
@@ -146,7 +148,7 @@ Consumers inject `Func` and invoke it with a category
}
```
-Only `AppType` and `LogLevel` bind to `AcLoggerOptions` (defaults: `Server` / `Info`). Extend the POCO as needed; the binding picks up new properties automatically. The legacy `LogWriters[]` array (config-reading ctor) is unused in this pattern.
+Only `AppType` and `LogLevel` bind to `AcLoggerOptions`. **`AppType` resolution order (2026-07):** appsettings value (if present) → the `AddAcLoggerFactory(defaultAppType)` parameter → `Server` (the parameter's own default). `AcLoggerOptions.AppType` is nullable — `null` means "not configured", so the host's code default can apply; a config value always wins. `LogLevel` default: `Info`. Extend the POCO as needed; the binding picks up new properties automatically. The legacy `LogWriters[]` array (config-reading ctor) is unused in this pattern.
### TLogger ctor requirement
diff --git a/AyCode.Services/Loggers/AcSignaRClientLogItemWriter.cs b/AyCode.Services/Loggers/AcSignaRClientLogItemWriter.cs
index 95d5d26..8e87fa3 100644
--- a/AyCode.Services/Loggers/AcSignaRClientLogItemWriter.cs
+++ b/AyCode.Services/Loggers/AcSignaRClientLogItemWriter.cs
@@ -10,16 +10,26 @@ namespace AyCode.Services.Loggers
public class AcSignaRClientLogItemWriter : AcLogItemWriterBase, IAcLogWriterClientBase
{
private readonly HubConnection _hubConnection;
+ private readonly string _fullHubName;
+
+ // A log-csatorna a SAJÁT hibáit nem logolhatja önmagán át (rekurzió) → Console.Error.
+ // Spam-védelem: hiba-állapotonként EGY jelentés; sikeres küldés után a kapu újraélesedik, és
+ // helyreálláskor kiírjuk, hány log-item veszett el. (Egyszerű mezők — a torz dupla-jelentés ártalmatlan.)
+ private bool _failureReported;
+ private long _droppedCount;
public AcSignaRClientLogItemWriter(string fullHubName, AppType appType = AppType.Web, LogLevel logLevel = LogLevel.Detail, string? categoryName = null) : base(appType, logLevel, categoryName)
{
+ _fullHubName = fullHubName;
+
_hubConnection = new HubConnectionBuilder()
.WithUrl(fullHubName)
//.WithAutomaticReconnect()
//.AddMessagePackProtocol(options => options.SerializerOptions = MessagePackSerializerOptions.Standard.WithSecurity(MessagePackSecurity.UntrustedData))
.Build();
- _hubConnection.StartAsync().Forget();
+ // A .Forget() logger nélkül elnyelné az induló kapcsolat hibáját → jelentő wrapper.
+ StartConnectionReportedAsync().Forget();
}
public async Task StartConnection()
@@ -42,16 +52,56 @@ namespace AyCode.Services.Loggers
protected override async void WriteLogItemCallback(AcLogItemClient logItem)
{
- //Ez fontos, mert a signalR nem küldi a DateTime.Kind-ot! A szeró oldalon kap egy utc DateTime-ot, ami a kliens lokális DateTime-ját tartalmazza! - J.
- logItem.TimeStampUtc = logItem.TimeStampUtc.ToUniversalTime();
+ // Teljes try/catch: async void — egy StartAsync/SendAsync-kivétel különben kezeletlenül szökne el.
+ try
+ {
+ //Ez fontos, mert a signalR nem küldi a DateTime.Kind-ot! A szeró oldalon kap egy utc DateTime-ot, ami a kliens lokális DateTime-ját tartalmazza! - J.
+ logItem.TimeStampUtc = logItem.TimeStampUtc.ToUniversalTime();
- await StartConnection();
+ await StartConnection();
- if (_hubConnection.State != HubConnectionState.Connected)
- return;
+ if (_hubConnection.State != HubConnectionState.Connected)
+ {
+ _droppedCount++;
+ ReportFailureOnce($"connection not available ({_hubConnection.State}), log item DROPPED", null);
+ return;
+ }
- //var refreshToken = Setting.UserBasicDetails?.RefreshToken ?? Guid.NewGuid().ToString("N");
- _hubConnection.SendAsync("AddLogItem", /*refreshToken,*/ logItem).Forget();
+ //var refreshToken = Setting.UserBasicDetails?.RefreshToken ?? Guid.NewGuid().ToString("N");
+ await _hubConnection.SendAsync("AddLogItem", /*refreshToken,*/ logItem);
+
+ ReportRecoveryIfNeeded();
+ }
+ catch (Exception ex)
+ {
+ _droppedCount++;
+ ReportFailureOnce("send failed, log item DROPPED", ex);
+ }
+ }
+
+ private async Task StartConnectionReportedAsync()
+ {
+ try { await StartConnection(); }
+ catch (Exception ex) { ReportFailureOnce("initial connect failed", ex); }
+ }
+
+ /// Hiba-állapotonként EGYSZER ír a Console.Error-ra (nem spamel) — a következő sikeres küldés élesíti újra.
+ private void ReportFailureOnce(string message, Exception? ex)
+ {
+ if (_failureReported) return;
+ _failureReported = true;
+
+ Console.Error.WriteLine($"[{nameof(AcSignaRClientLogItemWriter)}] {message}; url: {_fullHubName}; droppedCount: {_droppedCount}{(ex == null ? string.Empty : $"; ex: {ex.GetType().Name}: {ex.Message}")}");
+ }
+
+ /// Sikeres küldésnél: ha hiba-állapotból jöttünk vissza, jelentjük a kiesés alatt elveszett logok számát.
+ private void ReportRecoveryIfNeeded()
+ {
+ if (!_failureReported) return;
+ _failureReported = false;
+
+ Console.Error.WriteLine($"[{nameof(AcSignaRClientLogItemWriter)}] connection recovered; url: {_fullHubName}; DROPPED items while down: {_droppedCount}");
+ _droppedCount = 0;
}
}
}
diff --git a/AyCode.Services/docs/SIGNALR_DATASOURCE/SIGNALR_DATASOURCE_ISSUES.md b/AyCode.Services/docs/SIGNALR_DATASOURCE/SIGNALR_DATASOURCE_ISSUES.md
index b0912e6..08833ca 100644
--- a/AyCode.Services/docs/SIGNALR_DATASOURCE/SIGNALR_DATASOURCE_ISSUES.md
+++ b/AyCode.Services/docs/SIGNALR_DATASOURCE/SIGNALR_DATASOURCE_ISSUES.md
@@ -34,6 +34,43 @@ private Task SaveTrackingItemUnsafeAsync(TrackingItem ti)
---
+## ACCORE-SIGDS-I-H3M8: DataSource mutations and events run on background threads (WASM-safe; theoretical race under Blazor Server/MAUI)
+
+**Severity:** Minor (theoretical — no repro; current consumers are single-threaded WASM/Hybrid) · **Status:** Open · **Area:** `AcSignalRDataSource` threading model
+
+> **Note:** documents accepted current behaviour — not scheduled for change.
+
+### Description
+The DataSource does its collection work on non-UI threads:
+- **Bulk loads**: `LoadDataSourceFromResponseData` → `BeginUpdate()` → `PopulateMerge` → `EndUpdate()` runs on the
+ SignalR receive pipeline; `EndUpdate` raises the collection Reset notification on that thread.
+- **Per-item save confirms**: `ProcessSavedResponseItem` (thread-pool continuation via
+ `ContinueWith(TaskScheduler.Default)`, or the SignalR response callback) mutates the tracked item in place
+ (`CopyTo` under `lock (_syncRoot)` — no Begin/EndUpdate) and then fires `OnDataSourceItemChanged` on the same
+ background thread.
+
+Consequences for subscribers:
+- `OnDataSourceLoaded` / `OnDataSourceItemChanged` / `OnSyncingStateChanged` handlers execute on the event
+ thread — any UI work must be marshalled by the subscriber (e.g. `InvokeAsync`).
+- On single-threaded WebAssembly (the current deployment model) there is no true parallelism — safe.
+- Under Blazor Server / MAUI a theoretical race exists: a bulk-load mutation on the SignalR thread can
+ interleave with a UI-thread enumeration of the same collection (any render), risking
+ `InvalidOperationException` (collection modified) or an inconsistent view.
+
+### Root cause
+By design: the transport delivers responses on its own pipeline and the DataSource processes them where they
+arrive; marshalling is left to subscribers (the DataSource has no UI dependency).
+
+### Known workaround
+UI-layer subscribers marshal their render/refresh work via `InvokeAsync` (already the practice). A Server/MAUI
+deployment would additionally need the collection mutations themselves dispatched to the UI context — see fix direction.
+
+### Fix direction (if ever needed)
+Optional marshal delegate on the DataSource (e.g. a consumer-settable `Func` dispatcher) so
+loads/merges/`CopyTo` run on the consumer's UI context under Server/MAUI. Not planned while all consumers are WASM/Hybrid.
+
+---
+
## ACCORE-SIGDS-I-W2H6: BinarySearch is public but throws NotImplementedException
**Status:** Open