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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions SysManager/SysManager.Tests/ArchitectureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5451,6 +5451,70 @@ public void TheTimerResolutionQuery_BindsItsOutParametersCoarsestFinestCurrent()
private static partial Regex TimerQueryCall();


/// <summary>
/// A view-model that tracks its own "running" state must forward it to <c>IsBusy</c>.
/// </summary>
/// <remarks>
/// <c>NavItem</c> forwards <c>ViewModelBase.IsBusy</c> 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 <c>IsBusy</c> gets no bar at all.
/// <para>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&#160;MB
/// update download) and DNS &amp; 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.</para>
/// <para>The flag is the single source of truth: the fix is a generated <c>On…Changed</c> hook assigning
/// <c>IsBusy</c>, 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.</para>
/// </remarks>
[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<string, string>(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<string>();

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

/// <summary>An observable bool whose name says work is in progress.</summary>
[GeneratedRegex(@"\[ObservableProperty\][^;]{0,200}?private bool _(?<flag>is\w+(?:ing|Running|Loading));",
RegexOptions.Singleline)]
private static partial Regex RunningStateFlag();

/// <summary>
/// The snapshot cache lock may hold only the one-time cached queries, never a per-poll one.
/// </summary>
Expand Down
20 changes: 20 additions & 0 deletions SysManager/SysManager.Tests/NetworkRepairViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
6 changes: 3 additions & 3 deletions SysManager/SysManager/SysManager.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
<RootNamespace>SysManager</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>NU1603;NU1701</NoWarn>
<Version>1.76.12</Version>
<FileVersion>1.76.12.0</FileVersion>
<AssemblyVersion>1.76.12.0</AssemblyVersion>
<Version>1.76.13</Version>
<FileVersion>1.76.13.0</FileVersion>
<AssemblyVersion>1.76.13.0</AssemblyVersion>
<Product>SysManager</Product>
<Description>SysManager — Windows system monitoring toolkit by laurentiu021. Network, updates, health, logs, safe deep cleanup.</Description>
<PackageProjectUrl>https://github.com/laurentiu021/SystemManager</PackageProjectUrl>
Expand Down
2 changes: 2 additions & 0 deletions SysManager/SysManager/ViewModels/AboutViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/// <summary>Single release entry in the "What's new" history.</summary>
Expand Down
2 changes: 2 additions & 0 deletions SysManager/SysManager/ViewModels/DnsHostsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
3 changes: 3 additions & 0 deletions SysManager/SysManager/ViewModels/NetworkRepairViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
3 changes: 3 additions & 0 deletions SysManager/SysManager/ViewModels/SpeedTestViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
3 changes: 3 additions & 0 deletions SysManager/SysManager/ViewModels/TracerouteViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}