Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 22 additions & 16 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
root = true

[*.cs]
dotnet_diagnostic.CS8795.severity = none

dotnet_diagnostic.CA1062.severity = warning

dotnet_diagnostic.CA1515.severity = warning

dotnet_diagnostic.CA1416.severity = warning

dotnet_diagnostic.CA1812.severity = warning

dotnet_diagnostic.CA1031.severity = warning

# These warnings are excluded from TreatWarningsAsErrors in .csproj
# Set to suggestion to show them without failing builds
dotnet_diagnostic.CA1031.severity = suggestion
dotnet_diagnostic.CA1812.severity = suggestion
dotnet_diagnostic.CA2007.severity = suggestion
dotnet_diagnostic.CA1515.severity = suggestion
dotnet_diagnostic.CA1062.severity = suggestion

[**/Migrations/*.cs]
generated_code = true
dotnet_diagnostic.CA1861.severity = none
dotnet_diagnostic.CA1707.severity = none
dotnet_diagnostic.CA1812.severity = none
dotnet_diagnostic.CA1062.severity = none

[*]
charset = utf-8-bom
Expand Down Expand Up @@ -78,7 +80,7 @@ dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion

# Code quality rules
dotnet_code_quality_unused_parameters = all:warning
dotnet_code_quality_unused_parameters = all:suggestion
dotnet_remove_unnecessary_suppression_exclusions = none

# ReSharper properties
Expand Down Expand Up @@ -119,10 +121,14 @@ resharper_web_config_wrong_module_highlighting = warning
indent_style = space
indent_size = 2

# YAML files (GitHub Actions) - use LF to match .gitattributes
[*.{yml,yaml}]
indent_style = space
indent_size = 2
end_of_line = lf

# XML files
[*.{appxmanifest,asax,ascx,aspx,axaml,blockshader,c,c++,c++m,cc,ccm,cginc,compute,cp,cpp,cppm,cs,cshtml,cu,cuh,cxx,cxxm,dtd,fs,fsi,fsscript,fsx,fx,fxh,h,h++,hh,hlsl,hlsli,hlslinc,hp,hpp,hxx,icc,inc,inl,ino,ipp,ixx,master,ml,mli,mpp,mq4,mq5,mqh,mxx,nuspec,paml,razor,resw,resx,shader,shaderFoundry,skin,tcc,tpp,urtshader,usf,ush,uxml,vb,xaml,xamlx,xoml,xsd}]
indent_style = space
indent_size = 4
tab_width = 4


tab_width = 4
47 changes: 47 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Auto detect text files and perform LF normalization
* text=auto

# Force Windows line endings (CRLF) for C# and related files
*.cs text eol=crlf
*.csproj text eol=crlf
*.sln text eol=crlf
*.props text eol=crlf
*.targets text eol=crlf
*.config text eol=crlf
*.json text eol=crlf
*.xml text eol=crlf
*.xaml text eol=crlf
*.axaml text eol=crlf
*.resx text eol=crlf

# EditorConfig files
.editorconfig text eol=crlf

# Shell scripts should use LF
*.sh text eol=lf

# Batch/PowerShell scripts use CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf

# Markdown files
*.md text eol=crlf

# YAML files for GitHub Actions
*.yml text eol=lf
*.yaml text eol=lf

# Binary files
*.dll binary
*.exe binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.pdf binary
*.zip binary
*.7z binary
*.gz binary
*.nupkg binary
17 changes: 11 additions & 6 deletions LogAnalyzerForWindows/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,22 +96,27 @@ private static void ConfigureServices(IServiceCollection services)
services.AddDbContextFactory<LogAnalyzerDbContext>(options =>
{
options.UseSqlite(DbContextConfig.ConnectionString);

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting the default query tracking behavior to NoTracking at the DbContext level (line 99) is good for read-heavy operations, but it means write operations will need to explicitly attach entities or use tracking. Ensure that all write operations (e.g., SaveLogsAsync) are tested to verify they work correctly with this global setting.

This is generally a good optimization for this read-heavy application, but it's worth documenting this design decision.

Suggested change
options.UseSqlite(DbContextConfig.ConnectionString);
options.UseSqlite(DbContextConfig.ConnectionString);
// NOTE:
// This application is predominantly read-heavy, so we intentionally set the
// global query tracking behavior to NoTracking for performance reasons.
// Any write operations that rely on change tracking (for example, methods
// like SaveLogsAsync) must explicitly enable tracking (e.g., via AsTracking())
// or attach entities to the context before saving. Ensure such write paths
// are covered by tests to verify they behave correctly with this setting.

Copilot uses AI. Check for mistakes.
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});

services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IDialogService, DialogService>();

services.AddSingleton<ILogRepository, LogRepository>();
services.AddSingleton<IEmailService, EmailService>();
services.AddSingleton<IFileSystemService, FileSystemService>();
services.AddSingleton<ILogStatisticsService, LogStatisticsService>();
services.AddSingleton<ILogMonitor, LogMonitor>();
services.AddSingleton<ITrayIconService, TrayIconService>();
services.AddSingleton<ILogExportService, LogExportService>();

services.AddTransient<IDialogService, DialogService>();
services.AddTransient<IEmailService, EmailService>();
services.AddTransient<IFileSystemService, FileSystemService>();

services.AddTransient<MainWindowViewModel>();
services.AddTransient<SettingsViewModel>();
services.AddTransient<Func<ILogRepository, PaginationViewModel>>(sp =>
repository => new PaginationViewModel(repository));
services.AddTransient<DashboardViewModel>();
services.AddTransient<PaginationViewModel>();

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PaginationViewModel is registered as both Transient (line 116) and as a factory function (lines 118-119). This is redundant and could lead to confusion. The factory function creates instances directly using new, bypassing dependency injection for the PaginationViewModel itself (though it still gets the repository injected).

Consider removing the AddTransient<PaginationViewModel>() registration on line 116 since it's not being used - the factory function on lines 118-119 is the actual mechanism being used to create instances.

Suggested change
services.AddTransient<PaginationViewModel>();

Copilot uses AI. Check for mistakes.

services.AddSingleton<Func<ILogRepository, PaginationViewModel>>(sp =>
repository => new PaginationViewModel(repository));
}

private void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e)
Expand Down
83 changes: 83 additions & 0 deletions LogAnalyzerForWindows/Commands/AsyncRelayCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Windows.Input;
using Avalonia.Threading;

namespace LogAnalyzerForWindows.Commands;

internal sealed class AsyncRelayCommand : ICommand
{
private readonly Func<Task> _execute;
private readonly Func<bool>? _canExecute;
private bool _isExecuting;

public AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}

public event EventHandler? CanExecuteChanged;

public bool CanExecute(object? parameter) => !_isExecuting && (_canExecute?.Invoke() ?? true);

public async void Execute(object? parameter)
{
if (!CanExecute(parameter)) return;

_isExecuting = true;
OnCanExecuteChanged();

try
{
await _execute().ConfigureAwait(false);
}
finally
{
_isExecuting = false;
await Dispatcher.UIThread.InvokeAsync(OnCanExecuteChanged);
}
}
Comment on lines +22 to +38

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Execute method is marked as async void, which is generally appropriate for event handlers and ICommand implementations. However, any exceptions thrown during the execution of _execute() will be swallowed and not observable by the caller. Consider adding try-catch blocks around the await _execute() call to log exceptions or show error messages to the user.

The same applies to the generic AsyncRelayCommand<T> class (line 64).

Copilot uses AI. Check for mistakes.

public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

internal sealed class AsyncRelayCommand<T> : ICommand
{
private readonly Func<T?, Task> _execute;
private readonly Func<T?, bool>? _canExecute;
private bool _isExecuting;

public AsyncRelayCommand(Func<T?, Task> execute, Func<T?, bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}

public event EventHandler? CanExecuteChanged;

public bool CanExecute(object? parameter)
{
if (_isExecuting) return false;
if (_canExecute == null) return true;
return _canExecute(parameter is T t ? t : default);
}

public async void Execute(object? parameter)
{
if (!CanExecute(parameter)) return;

_isExecuting = true;
OnCanExecuteChanged();

try
{
await _execute(parameter is T t ? t : default).ConfigureAwait(false);
}
finally
{
_isExecuting = false;
await Dispatcher.UIThread.InvokeAsync(OnCanExecuteChanged);
}
}

public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
35 changes: 30 additions & 5 deletions LogAnalyzerForWindows/Database/LogAnalyzerDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity<LogEntryEntity>(entity =>
{
entity.HasKey(e => e.Id);

entity.Property(e => e.Timestamp);
entity.Property(e => e.Level).HasMaxLength(50);
entity.Property(e => e.Message);
Expand All @@ -36,11 +37,35 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.Property(e => e.CreatedAt);
entity.Property(e => e.SessionId).HasMaxLength(100);

entity.HasIndex(e => e.Timestamp);
entity.HasIndex(e => e.Level);
entity.HasIndex(e => e.EventId);
entity.HasIndex(e => e.Source);
entity.HasIndex(e => e.SessionId);
entity.HasIndex(e => new { e.SessionId, e.Timestamp })
.HasDatabaseName("IX_LogEntries_SessionId_Timestamp");

entity.HasIndex(e => new { e.SessionId, e.Level })
.HasDatabaseName("IX_LogEntries_SessionId_Level");

entity.HasIndex(e => new { e.SessionId, e.Source })
.HasDatabaseName("IX_LogEntries_SessionId_Source");

entity.HasIndex(e => new { e.Level, e.Timestamp })
.HasDatabaseName("IX_LogEntries_Level_Timestamp");

entity.HasIndex(e => e.Timestamp)
.HasDatabaseName("IX_LogEntries_Timestamp");

entity.HasIndex(e => e.Level)
.HasDatabaseName("IX_LogEntries_Level");

entity.HasIndex(e => e.EventId)
.HasDatabaseName("IX_LogEntries_EventId");

entity.HasIndex(e => e.Source)
.HasDatabaseName("IX_LogEntries_Source");

entity.HasIndex(e => e.SessionId)
.HasDatabaseName("IX_LogEntries_SessionId");

entity.HasIndex(e => e.CreatedAt)
.HasDatabaseName("IX_LogEntries_CreatedAt");
});
}
}
2 changes: 2 additions & 0 deletions LogAnalyzerForWindows/Database/Repositories/ILogRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,6 @@ Task<Dictionary<string, int>> GetLogsByLevelAsync(
int top = 10,
string? sessionId = null,
CancellationToken cancellationToken = default);

Task<int> DeleteSessionAsync(string sessionId);
}
Loading
Loading