From 54cc06fa87e776b30754a444307f5291cd650bf7 Mon Sep 17 00:00:00 2001 From: laurentiu021 Date: Fri, 4 Sep 2026 17:10:37 +0300 Subject: [PATCH] fix: show the sidebar progress bar on the five slowest tabs NavItem forwards ViewModelBase.IsBusy to the slim progress bar under a tab's name, which is the only sign -- while the user is on another tab -- that this one is working. Five view-models kept their own running flag and never assigned IsBusy, so their bar never appeared, and they are the slowest screens in the app: Speed Test (up to a minute), Traceroute (up to thirty hops), Network Repair (three netsh resets), About (an ~85 MB download) and DNS & Hosts. One generated hook each, copying the idiom DeepCleanupViewModel already uses, so the existing flag stays the single source of truth. Scope came from a corrected measurement. Looking for On...Changed hooks reported 16 unforwarded flags, which was wrong -- several tabs assign IsBusy inline in the command body, which is equally correct. Re-measured as "assigns IsBusy anywhere" the real list was three, plus two more once the flag pattern covered Running/Loading suffixes. Guarded by EveryViewModelThatTracksRunningState_ForwardsItToIsBusy, indifferent to HOW the assignment happens so the inline tabs keep passing, with the one non-tab match (AudioSessionRowViewModel.IsUserAdjusting, a slider drag) excluded with its reason. This also makes the README's progress-bar sentence true, which #2113 left standing on purpose rather than weakening it. --- CHANGELOG.md | 14 ++++ .../SysManager.Tests/ArchitectureTests.cs | 64 +++++++++++++++++++ .../NetworkRepairViewModelTests.cs | 20 ++++++ SysManager/SysManager/SysManager.csproj | 6 +- .../SysManager/ViewModels/AboutViewModel.cs | 2 + .../ViewModels/DnsHostsViewModel.cs | 2 + .../ViewModels/NetworkRepairViewModel.cs | 3 + .../ViewModels/SpeedTestViewModel.cs | 3 + .../ViewModels/TracerouteViewModel.cs | 3 + 9 files changed, 114 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ca298aa..848003bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,20 @@ That paragraph is not decoration: the release workflow copies each entry verbati the GitHub release body and the announcement discussion, so it is the first thing a prospective user reads. CI fails a pull request whose newest entry is missing it. +## [1.76.13] - 2026-09-04 + +Five of the slowest screens never showed the thin progress line under their name in the left-hand list, so if +you started something and switched away, nothing told you it was still going. Speed Test, Traceroute, Network +Repair, DNS & Hosts and the update download in About now show it like every other screen. + +### Fixed +- **The progress line appears on the five screens that were missing it.** Each of them already kept track of + whether it was working — it just never passed that on to the window frame, which is what draws the line. So + the tabs where you are most likely to walk away mid-job were the ones that looked idle: a speed test takes + up to a minute, a traceroute walks up to thirty hops, a network repair runs three resets in a row, and the + update download in About is around 85 MB. A check now fails the build if a screen tracks its own + working state without passing it on, so the next one cannot be added without it. + ## [1.76.12] - 2026-09-04 Two pieces of background work the app was doing for no reason. Nothing looks different; the app just asks diff --git a/SysManager/SysManager.Tests/ArchitectureTests.cs b/SysManager/SysManager.Tests/ArchitectureTests.cs index d925899b..4a92426b 100644 --- a/SysManager/SysManager.Tests/ArchitectureTests.cs +++ b/SysManager/SysManager.Tests/ArchitectureTests.cs @@ -5451,6 +5451,70 @@ public void TheTimerResolutionQuery_BindsItsOutParametersCoarsestFinestCurrent() private static partial Regex TimerQueryCall(); + /// + /// A view-model that tracks its own "running" state must forward it to IsBusy. + /// + /// + /// NavItem forwards ViewModelBase.IsBusy to the slim progress bar under the tab's name in + /// the sidebar — the only indication, while the user is looking at another tab, that this one is working. + /// A view-model that keeps a private running flag and never assigns IsBusy gets no bar at all. + /// Five tabs were in that state, and they were the slowest ones in the app: Speed Test (a full + /// up/down test), Traceroute (up to thirty hops), Network Repair (three netsh resets), About (an ~85 MB + /// update download) and DNS & Hosts. README promised the bar for "any long-running operation", so the + /// documentation was describing four view-models' behaviour as if it were all of them. + /// The flag is the single source of truth: the fix is a generated On…Changed hook assigning + /// IsBusy, never a second flag set alongside the first. Whether the assignment goes through the hook + /// or happens inline in the command is left open — several tabs predate the hook idiom and set it directly, + /// which is equally correct. + /// + [Fact] + public void EveryViewModelThatTracksRunningState_ForwardsItToIsBusy() + { + // Not a tab: a row inside the Volume Control list, with no sidebar entry to draw a bar under. Its + // flag means "the user is dragging this slider", which is not background work. + var notTabs = new Dictionary(StringComparer.Ordinal) + { + ["AudioSessionRowViewModel.cs"] = "a row inside Volume Control, not a tab; IsUserAdjusting is a " + + "drag gesture rather than work in progress", + }; + + var vmDir = Path.Combine(FindAppProjectDir(), "ViewModels"); + var withFlags = 0; + var missing = new List(); + + foreach (var file in Directory.GetFiles(vmDir, "*ViewModel.cs")) + { + var name = Path.GetFileName(file); + var code = WithoutComments(File.ReadAllText(file)); + var flags = RunningStateFlag().Matches(code).Select(m => m.Groups["flag"].Value).ToList(); + if (flags.Count == 0) continue; + + withFlags++; + if (notTabs.ContainsKey(name)) continue; + if (code.Contains("IsBusy =", StringComparison.Ordinal)) continue; + + missing.Add($"{name} tracks {string.Join(", ", flags)} but never assigns IsBusy"); + } + + // Vacuity floor: fourteen view-models carry such a flag today. A collapse means the pattern stopped + // matching the declaration shape, and this guard would pass having read nothing. + Assert.True(withFlags >= 12, + $"only {withFlags} view-models with a running-state flag were found — the declaration pattern no " + + "longer matches, so this guard proves nothing. Re-derive it before trusting a pass."); + + Assert.True(missing.Count == 0, + "these view-models track whether they are working and never tell the shell, so their tab shows no " + + "progress bar while the user is on another tab. Forward the existing flag — " + + "`partial void OnIsXChanged(bool value) => IsBusy = value;` — rather than adding a second flag. " + + "If the type is not a tab, add it to the exclusion list in this test WITH its reason:\n " + + string.Join("\n ", missing)); + } + + /// An observable bool whose name says work is in progress. + [GeneratedRegex(@"\[ObservableProperty\][^;]{0,200}?private bool _(?is\w+(?:ing|Running|Loading));", + RegexOptions.Singleline)] + private static partial Regex RunningStateFlag(); + /// /// The snapshot cache lock may hold only the one-time cached queries, never a per-poll one. /// diff --git a/SysManager/SysManager.Tests/NetworkRepairViewModelTests.cs b/SysManager/SysManager.Tests/NetworkRepairViewModelTests.cs index 4f9e1510..3b2842c7 100644 --- a/SysManager/SysManager.Tests/NetworkRepairViewModelTests.cs +++ b/SysManager/SysManager.Tests/NetworkRepairViewModelTests.cs @@ -34,6 +34,26 @@ public void DefaultState_NotRepairing() Assert.False(vm.RepairNeedsReboot); } + [Fact] + public void Repairing_DrivesIsBusy_SoTheSidebarShowsProgress() + { + // NavItem forwards ViewModelBase.IsBusy to the slim progress bar under the tab's name, which is the + // only sign — while the user is looking at another tab — that this one is working. Five tabs kept a + // running flag and never assigned IsBusy, so their bar never appeared; this asserts the generated + // On…Changed hook actually fires, which the source-shape guard in ArchitectureTests cannot. + // + // One behaviour test for the mechanism rather than five identical ones: the other four are the same + // one-line shape, and EveryViewModelThatTracksRunningState_ForwardsItToIsBusy is what keeps them there. + var vm = new NetworkRepairViewModel(NewShared()); + Assert.False(vm.IsBusy); + + vm.IsRepairing = true; + Assert.True(vm.IsBusy, "a repair is running and the shell was never told"); + + vm.IsRepairing = false; + Assert.False(vm.IsBusy, "the bar has to clear when the work finishes, or the tab looks stuck"); + } + [Fact] public async Task FlushDns_WhenUserDeclinesConfirm_DoesNothing() { diff --git a/SysManager/SysManager/SysManager.csproj b/SysManager/SysManager/SysManager.csproj index d3fa4282..8ae75ce3 100644 --- a/SysManager/SysManager/SysManager.csproj +++ b/SysManager/SysManager/SysManager.csproj @@ -10,9 +10,9 @@ SysManager true NU1603;NU1701 - 1.76.12 - 1.76.12.0 - 1.76.12.0 + 1.76.13 + 1.76.13.0 + 1.76.13.0 SysManager SysManager — Windows system monitoring toolkit by laurentiu021. Network, updates, health, logs, safe deep cleanup. https://github.com/laurentiu021/SystemManager diff --git a/SysManager/SysManager/ViewModels/AboutViewModel.cs b/SysManager/SysManager/ViewModels/AboutViewModel.cs index 676324fa..41992952 100644 --- a/SysManager/SysManager/ViewModels/AboutViewModel.cs +++ b/SysManager/SysManager/ViewModels/AboutViewModel.cs @@ -927,6 +927,8 @@ private static string BuildStamp() catch (UnauthorizedAccessException ex) { Log.Debug(ex, "About: access denied reading build date"); } return string.Empty; } + // Forward any running state to IsBusy so the sidebar progress indicator works + partial void OnIsDownloadingChanged(bool value) => IsBusy = value; } /// Single release entry in the "What's new" history. diff --git a/SysManager/SysManager/ViewModels/DnsHostsViewModel.cs b/SysManager/SysManager/ViewModels/DnsHostsViewModel.cs index f2ebad7e..c1f55807 100644 --- a/SysManager/SysManager/ViewModels/DnsHostsViewModel.cs +++ b/SysManager/SysManager/ViewModels/DnsHostsViewModel.cs @@ -614,4 +614,6 @@ protected override void Dispose(bool disposing) } base.Dispose(disposing); } + // Forward any running state to IsBusy so the sidebar progress indicator works + partial void OnIsDnsApplyingChanged(bool value) => IsBusy = value; } diff --git a/SysManager/SysManager/ViewModels/NetworkRepairViewModel.cs b/SysManager/SysManager/ViewModels/NetworkRepairViewModel.cs index f2b3b4f4..d6fee641 100644 --- a/SysManager/SysManager/ViewModels/NetworkRepairViewModel.cs +++ b/SysManager/SysManager/ViewModels/NetworkRepairViewModel.cs @@ -111,4 +111,7 @@ private async Task RunRepairAsync( { RepairStatus = $"✗ Error: {ex.Message}"; } finally { IsRepairing = false; } } + + // Forward any running state to IsBusy so the sidebar progress indicator works + partial void OnIsRepairingChanged(bool value) => IsBusy = value; } diff --git a/SysManager/SysManager/ViewModels/SpeedTestViewModel.cs b/SysManager/SysManager/ViewModels/SpeedTestViewModel.cs index 3a4a641c..335c90ca 100644 --- a/SysManager/SysManager/ViewModels/SpeedTestViewModel.cs +++ b/SysManager/SysManager/ViewModels/SpeedTestViewModel.cs @@ -245,4 +245,7 @@ protected override void Dispose(bool disposing) } base.Dispose(disposing); } + + // Forward any running state to IsBusy so the sidebar progress indicator works + partial void OnIsSpeedTestingChanged(bool value) => IsBusy = value; } diff --git a/SysManager/SysManager/ViewModels/TracerouteViewModel.cs b/SysManager/SysManager/ViewModels/TracerouteViewModel.cs index d339cefa..a616dd06 100644 --- a/SysManager/SysManager/ViewModels/TracerouteViewModel.cs +++ b/SysManager/SysManager/ViewModels/TracerouteViewModel.cs @@ -136,4 +136,7 @@ protected override void Dispose(bool disposing) } base.Dispose(disposing); } + + // Forward any running state to IsBusy so the sidebar progress indicator works + partial void OnIsTracingChanged(bool value) => IsBusy = value; }