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;
}