fix: thread safety, event marshaling, entry-point robustness, config alignment#396
Conversation
…alignment SVC-01/02: AppAlertService events marshaled to UI via SynchronizationContext SVC-21/23: NetworkRepairService + PerformanceService serialize PowerShellRunner access SVC-27: PowerShellRunner.LineReceived documented as thread-pool event SVC-40: StartupService catches RuntimeBinderException on dynamic COM SVC-42: StartupService stderr task guarded with 5s timeout VM-19: NetworkSharedState FlushPending bg-thread path documented as intentional VM-21: AppAlertsViewModel uses Application.Current.Dispatcher VM-28: AboutViewModel no longer auto-downloads updates without consent ENTRY-01: App.xaml.cs named pipe listener for tray-minimized activation ENTRY-03: MainWindow.xaml.cs hooks Application.Exit for ViewModel disposal CFG-01: SysManager.csproj version updated to 0.48.21 CFG-02: xunit 2.5.3 -> 2.9.3 in Tests + IntegrationTests CFG-03: dependabot.yml adds IntegrationTests directory
📝 WalkthroughWalkthroughThis PR enhances SysManager v0.48.22 with threading safety, event marshaling, and reliability improvements. Key changes include single-instance activation via named pipes, serialized PowerShell execution across services, SynchronizationContext-aware event delivery, defensive error/timeout handling, and proper ViewModel disposal on app exit. Version metadata and xUnit dependencies are also updated. ChangesThreading, Concurrency, and Reliability Improvements
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
SysManager/SysManager/Services/NetworkRepairService.cs (1)
25-39:⚠️ Potential issue | 🟠 Major | ⚡ Quick winSame semaphore-leak risk: subscription is outside
tryin all three methods.After
WaitAsync, handler attachment happens beforetry. Any exception during attach can leave_gatepermanently held.Suggested fix pattern (apply to FlushDnsAsync, ResetWinsockAsync, ResetTcpIpAsync)
await _gate.WaitAsync(ct).ConfigureAwait(false); var output = new System.Collections.Concurrent.ConcurrentQueue<string>(); void OnLine(PowerShellLine line) => output.Enqueue(line.Text); -_ps.LineReceived += OnLine; -try +var subscribed = false; +try { + _ps.LineReceived += OnLine; + subscribed = true; var exit = await _ps.RunProcessAsync("ipconfig.exe", "/flushdns", ct, PowerShellRunner.OemEncoding) .ConfigureAwait(false); @@ -finally { _ps.LineReceived -= OnLine; _gate.Release(); } +finally +{ + if (subscribed) _ps.LineReceived -= OnLine; + _gate.Release(); +}Also applies to: 47-61, 69-83
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@SysManager/SysManager/Services/NetworkRepairService.cs` around lines 25 - 39, The semaphore (_gate) is acquired before subscribing the handler (OnLine) to _ps.LineReceived, so if handler attachment throws the semaphore can remain held; update FlushDnsAsync, ResetWinsockAsync, and ResetTcpIpAsync to attach the LineReceived handler inside the try block after WaitAsync succeeds (or otherwise ensure attachment cannot throw outside try), and keep the existing finally to always unsubscribe (_ps.LineReceived -= OnLine) and release (_gate.Release()); this guarantees the semaphore is always released even if subscription or subsequent code throws.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@SysManager/SysManager/App.xaml.cs`:
- Around line 96-104: After successfully signaling the running instance via the
named pipe, return immediately so the method does not continue to the
process/window scan fallback; specifically, inside the try block that creates
the NamedPipeClientStream (using PipeName and PipeDirection.Out) and calls
client.Connect(...), add an early return right after the successful
client.Connect(...) call to ensure the Win32 fallback only runs when the pipe
path fails.
In `@SysManager/SysManager/MainWindow.xaml.cs`:
- Around line 21-29: The viewmodel may be disposed twice because both
OnApplicationExit (hooked to Application.Current.Exit) and MainWindow.OnClosed
call (DataContext as MainWindowViewModel)?.Dispose(); fix by making disposal
idempotent: add a private bool _disposed field and check/set it inside
MainWindowViewModel.Dispose() so subsequent calls return immediately, or
alternatively unsubscribe Application.Current.Exit in MainWindow.OnClosed
(Application.Current.Exit -= OnApplicationExit) before calling Dispose() to
guarantee a single invocation; reference MainWindowViewModel.Dispose(),
MainWindow.OnClosed, and OnApplicationExit/Application.Current.Exit when making
the change.
In `@SysManager/SysManager/Services/PerformanceService.cs`:
- Around line 167-172: The semaphore (_psGate) is acquired before the try and
the event handler (OnLine -> _ps.LineReceived += OnLine) is subscribed outside
the try, so if subscription throws the semaphore is never released; fix by
moving the handler subscription inside the try block immediately after await
_psGate.WaitAsync(ct) (i.e., acquire gate, then enter try where you subscribe
the handler), and ensure the finally block always removes the handler
(_ps.LineReceived -= OnLine) and releases the gate (_psGate.Release()); apply
the same pattern to the other methods that subscribe to _ps.LineReceived (the
blocks around RunProcessAsync and their corresponding OnLine handlers).
In `@SysManager/SysManager/ViewModels/AboutViewModel.cs`:
- Line 103: The UpdateStatus string currently always appends
LatestPublishedLabel in parentheses which yields empty "()" when
LatestPublishedLabel is empty; update the logic where UpdateStatus is set
(referencing UpdateStatus, LatestVersionLabel and LatestPublishedLabel) to
conditionally include the published label only when LatestPublishedLabel is
non-empty — e.g., build the suffix as either $" ({LatestPublishedLabel})" when
non-empty or "" when empty, then set UpdateStatus = $"Update available:
{LatestVersionLabel}{suffix}. Click Download to get it."; ensure
trimming/spacing remains correct.
---
Outside diff comments:
In `@SysManager/SysManager/Services/NetworkRepairService.cs`:
- Around line 25-39: The semaphore (_gate) is acquired before subscribing the
handler (OnLine) to _ps.LineReceived, so if handler attachment throws the
semaphore can remain held; update FlushDnsAsync, ResetWinsockAsync, and
ResetTcpIpAsync to attach the LineReceived handler inside the try block after
WaitAsync succeeds (or otherwise ensure attachment cannot throw outside try),
and keep the existing finally to always unsubscribe (_ps.LineReceived -= OnLine)
and release (_gate.Release()); this guarantees the semaphore is always released
even if subscription or subsequent code throws.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f0c4d9fb-ca21-458a-a439-cf1ede359a99
📒 Files selected for processing (15)
.github/dependabot.ymlCHANGELOG.mdSysManager/SysManager.IntegrationTests/SysManager.IntegrationTests.csprojSysManager/SysManager.Tests/SysManager.Tests.csprojSysManager/SysManager/App.xaml.csSysManager/SysManager/MainWindow.xaml.csSysManager/SysManager/Services/AppAlertService.csSysManager/SysManager/Services/NetworkRepairService.csSysManager/SysManager/Services/PerformanceService.csSysManager/SysManager/Services/PowerShellRunner.csSysManager/SysManager/Services/StartupService.csSysManager/SysManager/SysManager.csprojSysManager/SysManager/ViewModels/AboutViewModel.csSysManager/SysManager/ViewModels/AppAlertsViewModel.csSysManager/SysManager/ViewModels/NetworkSharedState.cs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Build & unit tests
- GitHub Check: Analyze (csharp)
🔇 Additional comments (13)
SysManager/SysManager/ViewModels/NetworkSharedState.cs (1)
297-300: LGTM!SysManager/SysManager/Services/StartupService.cs (2)
114-117: LGTM!
432-434: LGTM!SysManager/SysManager/SysManager.csproj (1)
14-16: LGTM!SysManager/SysManager.IntegrationTests/SysManager.IntegrationTests.csproj (1)
16-16: LGTM!SysManager/SysManager.Tests/SysManager.Tests.csproj (1)
16-16: LGTM!.github/dependabot.yml (1)
40-50: LGTM!CHANGELOG.md (1)
9-45: LGTM!SysManager/SysManager/Services/PerformanceService.cs (1)
30-30: 🏗️ Heavy liftPowerShellRunner is Transient, not shared across services.
The DI configuration in
ServiceRegistration.csline 22 registersPowerShellRunneras Transient:services.AddTransient<PowerShellRunner>();The inline comment explicitly states: "PowerShellRunner is Transient — each consumer gets its own instance to avoid LineReceived event cross-talk between tabs."
Since each
PerformanceServiceinstance receives its ownPowerShellRunnerinstance, the instance-level_psGatesemaphore does not need to protect against cross-service access. The gate protects only concurrent calls within a single service instance, which is appropriate.The semaphore usage is not uniform across all methods in
PerformanceService—some methods (e.g.,DuplicateSchemeAsyncat line 217) properly await the gate before subscribing to events, while others (e.g.,GetActivePlanAsyncat line 167) subscribe first and only release the gate in the finally block without acquiring it. However, this inconsistency does not create a cross-service serialization gap because runners are not shared.SysManager/SysManager/App.xaml.cs (1)
6-7: LGTM!Also applies to: 19-23, 77-84, 123-153
SysManager/SysManager/Services/AppAlertService.cs (1)
23-39: LGTM!Also applies to: 176-176, 190-208
SysManager/SysManager/ViewModels/AppAlertsViewModel.cs (1)
6-7: LGTM!Also applies to: 32-35
SysManager/SysManager/Services/PowerShellRunner.cs (1)
24-29: LGTM!
| // Try named pipe first — works even when the window is hidden (tray mode) | ||
| try | ||
| { | ||
| using var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out); | ||
| client.Connect(timeout: 2000); | ||
| // Connection alone signals the running instance to activate | ||
| } | ||
| catch (TimeoutException) { /* pipe not available, fall back to window activation */ } | ||
| catch (IOException) { /* pipe not available */ } |
There was a problem hiding this comment.
Return immediately after a successful pipe signal.
Lines 96-104 still fall through into the process/window scan after client.Connect(...) succeeds, so a normal second launch can trigger both activation paths instead of using the Win32 path only as fallback. Adding an early return; keeps the old logic as a true backup path.
Proposed fix
try
{
using var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out);
client.Connect(timeout: 2000);
// Connection alone signals the running instance to activate
+ return;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Try named pipe first — works even when the window is hidden (tray mode) | |
| try | |
| { | |
| using var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out); | |
| client.Connect(timeout: 2000); | |
| // Connection alone signals the running instance to activate | |
| } | |
| catch (TimeoutException) { /* pipe not available, fall back to window activation */ } | |
| catch (IOException) { /* pipe not available */ } | |
| // Try named pipe first — works even when the window is hidden (tray mode) | |
| try | |
| { | |
| using var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out); | |
| client.Connect(timeout: 2000); | |
| // Connection alone signals the running instance to activate | |
| return; | |
| } | |
| catch (TimeoutException) { /* pipe not available, fall back to window activation */ } | |
| catch (IOException) { /* pipe not available */ } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SysManager/SysManager/App.xaml.cs` around lines 96 - 104, After successfully
signaling the running instance via the named pipe, return immediately so the
method does not continue to the process/window scan fallback; specifically,
inside the try block that creates the NamedPipeClientStream (using PipeName and
PipeDirection.Out) and calls client.Connect(...), add an early return right
after the successful client.Connect(...) call to ensure the Win32 fallback only
runs when the pipe path fails.
| // Ensure ViewModel disposal even if OnClosed is not called (e.g. app shutdown) | ||
| if (Application.Current != null) | ||
| Application.Current.Exit += OnApplicationExit; | ||
| } | ||
|
|
||
| private void OnApplicationExit(object sender, ExitEventArgs e) | ||
| { | ||
| (DataContext as MainWindowViewModel)?.Dispose(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether MainWindowViewModel.Dispose() is idempotent and whether duplicate disposal is currently possible.
set -euo pipefail
echo "== MainWindow exit/dispose call sites =="
fd -i '^MainWindow\.xaml\.cs$' --exec rg -n -C3 --type=cs 'Exit \+=|OnApplicationExit|OnClosed|Dispose\s*\(' {}
echo
echo "== MainWindowViewModel dispose implementation =="
for f in $(fd -i '^MainWindowViewModel\.cs$'); do
echo "-- $f --"
rg -n -C5 --type=cs '\bclass\s+MainWindowViewModel\b|\bvoid\s+Dispose\s*\(|\bbool\s+_disposed\b|Interlocked|GC\.SuppressFinalize' "$f"
done
echo
echo "Expected verification result:"
echo "1) Dispose implementation has an idempotency guard (_disposed/Interlocked/etc.), OR"
echo "2) MainWindow is updated to ensure dispose is invoked exactly once."Repository: laurentiu021/SystemManager
Length of output: 2067
Add an idempotency guard to MainWindowViewModel.Dispose() or ensure single invocation.
On normal shutdown, both OnApplicationExit (line 28) and OnClosed (line 94) call Dispose() on the same MainWindowViewModel instance. The Dispose() implementation has no guard against multiple calls and will attempt to dispose already-disposed items, risking ObjectDisposedException.
Either:
- Add a
_disposedboolean guard inMainWindowViewModel.Dispose()to make it idempotent, or - Unsubscribe from
Application.Current.ExitinOnClosedto ensure disposal runs exactly once.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SysManager/SysManager/MainWindow.xaml.cs` around lines 21 - 29, The viewmodel
may be disposed twice because both OnApplicationExit (hooked to
Application.Current.Exit) and MainWindow.OnClosed call (DataContext as
MainWindowViewModel)?.Dispose(); fix by making disposal idempotent: add a
private bool _disposed field and check/set it inside
MainWindowViewModel.Dispose() so subsequent calls return immediately, or
alternatively unsubscribe Application.Current.Exit in MainWindow.OnClosed
(Application.Current.Exit -= OnApplicationExit) before calling Dispose() to
guarantee a single invocation; reference MainWindowViewModel.Dispose(),
MainWindow.OnClosed, and OnApplicationExit/Application.Current.Exit when making
the change.
| await _psGate.WaitAsync(ct).ConfigureAwait(false); | ||
| var lines = new List<string>(); | ||
| void OnLine(PowerShellLine l) => lines.Add(l.Text); | ||
| _ps.LineReceived += OnLine; | ||
| try { await _ps.RunProcessAsync("powercfg.exe", "/getactivescheme", ct, PowerShellRunner.OemEncoding); } | ||
| finally { _ps.LineReceived -= OnLine; } | ||
| finally { _ps.LineReceived -= OnLine; _psGate.Release(); } |
There was a problem hiding this comment.
Wrap handler subscription in the same try/finally as gate ownership.
Right now, WaitAsync succeeds before try, and handler subscription also happens before try. If subscription throws (e.g., disposal race), the semaphore is never released, blocking future calls.
Suggested pattern (apply to all four methods)
await _psGate.WaitAsync(ct).ConfigureAwait(false);
var lines = new List<string>();
void OnLine(PowerShellLine l) => lines.Add(l.Text);
-_ps.LineReceived += OnLine;
-try { await _ps.RunProcessAsync("powercfg.exe", "/getactivescheme", ct, PowerShellRunner.OemEncoding); }
-finally { _ps.LineReceived -= OnLine; _psGate.Release(); }
+var subscribed = false;
+try
+{
+ _ps.LineReceived += OnLine;
+ subscribed = true;
+ await _ps.RunProcessAsync("powercfg.exe", "/getactivescheme", ct, PowerShellRunner.OemEncoding).ConfigureAwait(false);
+}
+finally
+{
+ if (subscribed) _ps.LineReceived -= OnLine;
+ _psGate.Release();
+}Also applies to: 217-220, 238-243, 468-477
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SysManager/SysManager/Services/PerformanceService.cs` around lines 167 - 172,
The semaphore (_psGate) is acquired before the try and the event handler (OnLine
-> _ps.LineReceived += OnLine) is subscribed outside the try, so if subscription
throws the semaphore is never released; fix by moving the handler subscription
inside the try block immediately after await _psGate.WaitAsync(ct) (i.e.,
acquire gate, then enter try where you subscribe the handler), and ensure the
finally block always removes the handler (_ps.LineReceived -= OnLine) and
releases the gate (_psGate.Release()); apply the same pattern to the other
methods that subscribe to _ps.LineReceived (the blocks around RunProcessAsync
and their corresponding OnLine handlers).
| // gracefully if blocked. | ||
| if (!IsDownloading && DownloadedPath == null) | ||
| _ = DownloadAsync(); | ||
| UpdateStatus = $"Update available: {LatestVersionLabel} ({LatestPublishedLabel}). Click Download to get it."; |
There was a problem hiding this comment.
Avoid empty-date parentheses in update status text.
When LatestPublishedLabel is empty, this renders (...) in the UI. Build the status string conditionally to avoid awkward text.
Proposed fix
- UpdateStatus = $"Update available: {LatestVersionLabel} ({LatestPublishedLabel}). Click Download to get it.";
+ var publishedSuffix = string.IsNullOrWhiteSpace(LatestPublishedLabel)
+ ? string.Empty
+ : $" ({LatestPublishedLabel})";
+ UpdateStatus = $"Update available: {LatestVersionLabel}{publishedSuffix}. Click Download to get it.";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| UpdateStatus = $"Update available: {LatestVersionLabel} ({LatestPublishedLabel}). Click Download to get it."; | |
| var publishedSuffix = string.IsNullOrWhiteSpace(LatestPublishedLabel) | |
| ? string.Empty | |
| : $" ({LatestPublishedLabel})"; | |
| UpdateStatus = $"Update available: {LatestVersionLabel}{publishedSuffix}. Click Download to get it."; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SysManager/SysManager/ViewModels/AboutViewModel.cs` at line 103, The
UpdateStatus string currently always appends LatestPublishedLabel in parentheses
which yields empty "()" when LatestPublishedLabel is empty; update the logic
where UpdateStatus is set (referencing UpdateStatus, LatestVersionLabel and
LatestPublishedLabel) to conditionally include the published label only when
LatestPublishedLabel is non-empty — e.g., build the suffix as either $"
({LatestPublishedLabel})" when non-empty or "" when empty, then set UpdateStatus
= $"Update available: {LatestVersionLabel}{suffix}. Click Download to get it.";
ensure trimming/spacing remains correct.
…vice (#419) ## Summary **Batch 2** of the QA bug fix series. ### Issue #396 — CancellationTokenSource not disposed Added Dispose(bool) override to **8 ViewModels** that had CTS fields but no cleanup on dispose: - AppUpdatesViewModel - DiskAnalyzerViewModel - DriversViewModel - DuplicateFileViewModel - LogsViewModel - SpeedTestViewModel - TracerouteViewModel - UninstallerViewModel Each override disposes the CTS field and calls �ase.Dispose(disposing). The existing VMs that already had Dispose (CleanupVM, DeepCleanupVM, NetworkVM, SystemHealthVM, WindowsUpdateVM) were verified and left unchanged. ### Issue #413 — Bare catch in UpdateService Replaced bare \catch\ blocks with specific exception types + Serilog logging: - **GetRecentAsync**: catches \OperationCanceledException\, \HttpRequestException\, \JsonException\ - **DownloadAsync**: catches \OperationCanceledException\, \HttpRequestException\, \IOException\ - Cleanup catch blocks in DownloadAsync now catch \IOException\ and \UnauthorizedAccessException\ instead of bare catch ### Testing - Main project builds with 0 errors - No behavior changes — only proper resource cleanup and specific exception handling - CI will validate full test suite Closes #396, Closes #413 Co-authored-by: laurentiu021 <laurentiu021@users.noreply.github.com>
Adds CHANGELOG entries for all 9 releases from the QA bug fix session: - **v0.28.16** — Dispose lifecycle (#395, #410) - **v0.28.17** — CTS disposal + bare catch (#396, #413) - **v0.28.18** — Input validation + null checks (#397, #398) - **v0.28.19** — JSON error handling (#400) - **v0.28.20** — Drive scanning + cache eviction + ConfigureAwait (#401, #402, #403) - **v0.28.21** — Audit logging + error messages (#405, #407) - **v0.28.22** — SHA256 verification (#408, #409) - **v0.28.23** — Service timeout + snapshot persist + traceroute DNS (#414, #415, #416) - **v0.28.24** — Accessibility (#411) 18 bugs fixed in total. Co-authored-by: laurentiu021 <laurentiu021@users.noreply.github.com>
…alignment (#396) SVC-01/02: AppAlertService events marshaled to UI via SynchronizationContext SVC-21/23: NetworkRepairService + PerformanceService serialize PowerShellRunner access SVC-27: PowerShellRunner.LineReceived documented as thread-pool event SVC-40: StartupService catches RuntimeBinderException on dynamic COM SVC-42: StartupService stderr task guarded with 5s timeout VM-19: NetworkSharedState FlushPending bg-thread path documented as intentional VM-21: AppAlertsViewModel uses Application.Current.Dispatcher VM-28: AboutViewModel no longer auto-downloads updates without consent ENTRY-01: App.xaml.cs named pipe listener for tray-minimized activation ENTRY-03: MainWindow.xaml.cs hooks Application.Exit for ViewModel disposal CFG-01: SysManager.csproj version updated to 0.48.21 CFG-02: xunit 2.5.3 -> 2.9.3 in Tests + IntegrationTests CFG-03: dependabot.yml adds IntegrationTests directory Co-authored-by: laurentiu021 <laurentiu021@users.noreply.github.com>
Summary
Fixes all 13 remaining findings from the FINAL_COMPLETE_AUDIT.
Service Thread-Safety (SVC-01/02, SVC-21/23, SVC-27, SVC-40, SVC-42)
ViewModel & Entry-Point (VM-19, VM-21, VM-28, ENTRY-01, ENTRY-03)
Config (CFG-01, CFG-02, CFG-03)
Build
0 errors, 0 new warnings.
Files changed (14)
Services: AppAlertService.cs, NetworkRepairService.cs, PerformanceService.cs, PowerShellRunner.cs, StartupService.cs
ViewModels: AboutViewModel.cs, AppAlertsViewModel.cs, NetworkSharedState.cs
Entry: App.xaml.cs, MainWindow.xaml.cs
Config: SysManager.csproj, SysManager.Tests.csproj, IntegrationTests.csproj, dependabot.yml
Docs: CHANGELOG.md
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Chores