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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ 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.130.50] - 2026-10-21

**Settings Watchdog and Camera/Mic/Location look again each time you open them, so a link that sends you to a
changed setting shows it, and "In use now" means now.**

### Fixed

- **Settings Watchdog and Camera/Mic/Location showed what they read the first time.** Both read when the tab was first
built and then only on Refresh, and SysManager can stay in the notification area for days. Undo Changes and Recent
Changes send you to Settings Watchdog because a watched setting changed, and the page still said "All watched
settings match your baseline." with Restore off. Camera/Mic/Location kept saying "In use now" for an app that had let
go of the camera long ago. Both look again each time you open them. Since 1.32.0 for Camera/Mic/Location and
1.48.0 for Settings Watchdog.
- **Settings Watchdog's CSV said every change could be restored.** Its "Can restore" column said "yes" for a setting
that was not set when the baseline was saved, which Restore does not put back. It says "no" for those now. Since
1.123.0.

## [1.130.49] - 2026-10-21

**Edge/OneDrive Remover says that Edge will show "Your browser is managed by your organization" while it is
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1164,7 +1164,8 @@ Answers "what is actually using my space?" by listing the biggest files in one p
- Shows which apps recently used your **camera, microphone, or location**, and when
- Reads the Windows access history (CapabilityAccessManager consent store) — covers
both Store apps and desktop programs
- Devices **in use right now** are flagged and sorted to the top
- Devices **in use right now** are flagged and sorted to the top, and the tab reads the history
again each time you open it, so "In use now" means now
- If Windows does not let SysManager read a device's history, the tab names that device
instead of listing the others as if it had been checked, and a Refresh that reads
nothing at all keeps the previous list on screen
Expand All @@ -1189,10 +1190,12 @@ Answers "what is actually using my space?" by listing the biggest files in one p
aside
- **Check now** re-reads the live values and lists any drift in plain language, one row per
setting with its category and **Was** / **Now** columns — e.g. *Diagnostic data (telemetry):
Was "Off (Security)", Now "Full"*
Was "Off (Security)", Now "Full"*. The tab also checks each time you open it, so following a
link from Undo Changes or Recent Changes shows the change that sent you here
- **Export CSV** saves the list of changes before you restore them. Restore overwrites the
"now" column, so this file is the only record of what Windows changed — which is why the
button sits to the left of Restore
button sits to the left of Restore. Its "Can restore" column says what Restore will do: "no"
for a setting that was not set when you saved the baseline
- **Restore changed** writes the drifted settings back to your baseline values in
one step, after one confirmation. Telemetry, activity history and Widgets are machine-wide
policies and need administrator rights, surfaced not crashed. A setting that was not set at all
Expand Down
11 changes: 11 additions & 0 deletions SysManager/SysManager.Tests/CsvExportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,17 @@ public void DriftToCsv_RecordsWhetherEachRowCanBeRestored()
Assert.EndsWith(",no\r\n", csv, StringComparison.Ordinal);
}

[Fact]
public void DriftToCsv_ASettingThatWasNotSetInTheBaseline_CannotBeRestored()
{
// Every catalog entry can be restored in principle, and Restore leaves this one alone, because putting
// "Not set" back would delete a value and the watchdog never deletes one. The column said "yes" (#2837).
var csv = SettingsWatchdogService.ToCsv([
new SettingDrift(TelemetrySetting(), BaselineValue: null, CurrentValue: 3)], Invariant);

Assert.EndsWith(",no\r\n", csv, StringComparison.Ordinal);
}

[Fact]
public void DriftToCsv_WithNoDrifts_IsHeaderOnly()
{
Expand Down
50 changes: 50 additions & 0 deletions SysManager/SysManager.Tests/PrivacyMonitorViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,54 @@ public async Task AReadThatWorksAfterAFailedOne_ClearsTheFailure()
Assert.Equal("No access recorded", vm.EmptyTitle);
Assert.Equal("No camera, microphone, or location access has been recorded yet.", vm.StatusMessage);
}

// ── read again when shown (#2837) ──

[Fact]
public async Task ShowingTheTabAgain_ReadsAgain_SoACameraLetGoOfNoLongerReadsInUse()
{
// The tab read once, when it was built, and a later visit still said "In use now" for an app that had closed.
var camInUse = Cam with { InUse = true };
var reads = 0;
var vm = await SettledVm(() => ++reads == 1 ? Report([camInUse]) : Report([Cam]));
Assert.True(Assert.Single(vm.Entries).InUse);

vm.IsActive = true;
await vm.ShownRefresh;

Assert.Equal(2, reads);
Assert.False(Assert.Single(vm.Entries).InUse);
Assert.DoesNotContain("in use right now", vm.StatusMessage, StringComparison.Ordinal);
}

[Fact]
public async Task LeavingTheTab_ReadsNothing()
{
var reads = 0;
var vm = await SettledVm(() => { reads++; return Report([Cam]); });

vm.IsActive = true;
await vm.ShownRefresh;
vm.IsActive = false;
await vm.ShownRefresh;

Assert.Equal(2, reads);
}

[Fact]
public async Task ShowingTheTab_WhileARefreshReads_StartsNoSecondRead()
{
var held = new TaskCompletionSource<PrivacyAccessReport>(TaskCreationOptions.RunContinuationsAsynchronously);
var reads = 0;
var vm = new PrivacyMonitorViewModel(_ => ++reads == 1 ? Task.FromResult(Report([Cam])) : held.Task);
await vm.InitializationComplete;
var refresh = vm.RefreshCommand.ExecuteAsync(null);

vm.IsActive = true;
held.SetResult(Report([Cam]));
await refresh;
await vm.ShownRefresh;

Assert.Equal(2, reads);
}
}
44 changes: 44 additions & 0 deletions SysManager/SysManager.Tests/SettingsWatchdogViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -557,4 +557,48 @@ public async Task Refresh_WhenNothingMoved_MarksNoDrift()
Assert.Empty(vm.Drifts);
Assert.False(vm.HasDrift);
}

// ── look again when shown (#2837) ──

[Fact]
public void ShowingTheTabAgain_LooksAgain_SoASettingWindowsResetSinceIsListed()
{
// Undo Changes and Recent Changes send the user here because a watched setting changed, and the page still
// showed the look from before: "All watched settings match your baseline.", with Restore off.
var svc = NewService();
var vm = new SettingsWatchdogViewModel(svc);
Assert.False(vm.HasDrift);

var drift = Drift("ads");
svc.DetectDrift(Arg.Any<BaselineSnapshot>(), Arg.Any<IReadOnlyDictionary<string, int?>>()).Returns([drift]);
vm.IsActive = true;

Assert.True(vm.HasDrift);
Assert.Equal(drift, Assert.Single(vm.Drifts).Drift);
Assert.True(vm.RestoreSelectedCommand.CanExecute(null));
Assert.Equal("1 setting(s) changed since your baseline.", vm.StatusMessage);
}

[Fact]
public void LeavingTheTab_LooksAtNothing()
{
var svc = NewService();
var vm = new SettingsWatchdogViewModel(svc);
vm.IsActive = true;
svc.ClearReceivedCalls();

vm.IsActive = false;

svc.DidNotReceive().ReadCurrent();
}

[Fact]
public void ADriftWithNoValueInTheBaseline_IsNotOfferedForRestore_OnItsRow()
{
// Restore never puts it back, because the watchdog never deletes a value (#2594).
var notSet = new SettingDrift(Setting("ads"), BaselineValue: null, CurrentValue: 1, CanRestore: true);
var vm = new SettingsWatchdogViewModel(NewService(notSet));

Assert.False(Assert.Single(vm.Drifts).CanRestore);
}
}
4 changes: 3 additions & 1 deletion SysManager/SysManager/Services/SettingsWatchdogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,9 @@ public static string ToCsv(IEnumerable<SettingDrift> drifts, CultureInfo culture
d.Setting.Description,
d.BaselineLabel,
d.CurrentLabel,
d.CanRestore ? "yes" : "no");
// What Restore will do. CanRestore is true for every catalog entry, and a drift the baseline holds no
// value for is never put back, because the watchdog never deletes a value (#2594, #2837).
d.CanWriteBack ? "yes" : "no");
}
return sb.ToString();
}
Expand Down
6 changes: 3 additions & 3 deletions SysManager/SysManager/SysManager.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
<NoWarn>$(NoWarn);NU1603;NU1701</NoWarn>
<!-- The documentation file exists for the compiler's checks; it is not part of what ships. -->
<PublishDocumentationFile>false</PublishDocumentationFile>
<Version>1.130.49</Version>
<FileVersion>1.130.49.0</FileVersion>
<AssemblyVersion>1.130.49.0</AssemblyVersion>
<Version>1.130.50</Version>
<FileVersion>1.130.50.0</FileVersion>
<AssemblyVersion>1.130.50.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/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,8 @@ internal static void SetActive(object content, bool active)
case PrivacyViewModel pv: pv.IsActive = active; break;
case UndoChangesViewModel uc: uc.IsActive = active; break;
case RecentChangesViewModel rc: rc.IsActive = active; break;
case SettingsWatchdogViewModel sw: sw.IsActive = active; break;
case PrivacyMonitorViewModel mo: mo.IsActive = active; break;
}
}

Expand Down
20 changes: 20 additions & 0 deletions SysManager/SysManager/ViewModels/PrivacyMonitorViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ public sealed partial class PrivacyMonitorViewModel : ViewModelBase
[ObservableProperty] private string _emptyTitle = "No access recorded";
[ObservableProperty] private string _emptyMessage = "Windows hasn't logged any camera, microphone, or location access yet.";

/// <summary>
/// True while the tab is on screen. Set by <see cref="MainWindowViewModel.SetActive"/>; becoming visible reads
/// again.
/// </summary>
/// <remarks>
/// The tab read the history when it was built and then only on Refresh, so a later visit still said "In use now"
/// for an app that had let go of the camera long ago, on the tab someone opens to ask what is watching them right
/// now (#2837).
/// </remarks>
[ObservableProperty] private bool _isActive;

/// <summary>The read the tab started when it was last shown. Internal so a test can await it.</summary>
internal Task ShownRefresh { get; private set; } = Task.CompletedTask;

public PrivacyMonitorViewModel(PrivacyMonitorService service) : this(service.ReadAsync) { }

/// <summary>Test seam: the consent-store read, so a failed one can be supplied without the registry.</summary>
Expand All @@ -61,6 +75,12 @@ private void OnVmPropertyChanged(object? sender, System.ComponentModel.PropertyC
if (e.PropertyName is nameof(IsBusy)) RefreshCommand.NotifyCanExecuteChanged();
}

partial void OnIsActiveChanged(bool value)
{
// Not while the first read is running, which is the fresh one, nor beside a Refresh still reading.
if (value && InitializationComplete.IsCompleted && NotBusy) ShownRefresh = RefreshAsync();
}

[RelayCommand(CanExecute = nameof(NotBusy))]
private async Task RefreshAsync()
{
Expand Down
22 changes: 21 additions & 1 deletion SysManager/SysManager/ViewModels/SettingsWatchdogViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ public sealed partial class SettingsWatchdogViewModel : ViewModelBase
[ObservableProperty] private bool _hasDrift;
[ObservableProperty] private bool _isElevated;

/// <summary>
/// True while the tab is on screen. Set by <see cref="MainWindowViewModel.SetActive"/>; becoming visible looks
/// again.
/// </summary>
/// <remarks>
/// The tab read the settings when it was built and then only on Refresh, and SysManager can stay in the
/// notification area for days. Undo Changes and Recent Changes send the user here because a watched setting
/// changed, and the page still showed the look from before it did (#2837).
/// </remarks>
[ObservableProperty] private bool _isActive;

// True when a baseline file is there and could not be read or used. It is not "no baseline yet": saving
// over it without asking replaced a baseline the user could not see (#2521).
private bool _baselineUnusable;
Expand All @@ -59,6 +70,13 @@ public SettingsWatchdogViewModel(ISettingsWatchdogService service)
InitializeAsync(() => { Refresh(); return System.Threading.Tasks.Task.CompletedTask; });
}

partial void OnIsActiveChanged(bool value)
{
// Not while the first look is still running: the tab is shown the moment it is built, and that look is the
// fresh one.
if (value && InitializationComplete.IsCompleted) Refresh();
}

[RelayCommand]
private void RelaunchAsAdmin()
{
Expand Down Expand Up @@ -248,7 +266,9 @@ public sealed partial class DriftRow(SettingDrift drift) : ObservableObject
public string Description => Drift.Setting.Description;
public string BaselineLabel => Drift.BaselineLabel;
public string CurrentLabel => Drift.CurrentLabel;
public bool CanRestore => Drift.CanRestore;

// What Restore will do: a drift the baseline holds no value for is never put back (#2594).
public bool CanRestore => Drift.CanWriteBack;
}

/// <summary>
Expand Down