From 277d610610af2b896df7dcab5aafb5b159836d50 Mon Sep 17 00:00:00 2001 From: laurentiu021 Date: Wed, 21 Oct 2026 22:47:57 +0300 Subject: [PATCH] fix: Settings Watchdog and Camera/Mic/Location look again each time they are shown, and the drift CSV says what Restore will do - SettingsWatchdogViewModel and PrivacyMonitorViewModel have an IsActive flag, set by MainWindowViewModel.SetActive, that looks again when the tab comes on screen once the first look is done, as Undo Changes and Recent Changes do. Camera/Mic/Location does not start a read beside a Refresh still reading. Both tabs read only when built and on Refresh, so a link from Undo Changes showed the look from before the change, and "In use now" stayed. - SettingsWatchdogService.ToCsv writes CanWriteBack in "Can restore", and the drift row's CanRestore is CanWriteBack: a setting not set in the baseline is never put back, and every catalog entry has CanRestore. --- CHANGELOG.md | 17 +++++++ README.md | 9 ++-- SysManager/SysManager.Tests/CsvExportTests.cs | 11 ++++ .../PrivacyMonitorViewModelTests.cs | 50 +++++++++++++++++++ .../SettingsWatchdogViewModelTests.cs | 44 ++++++++++++++++ .../Services/SettingsWatchdogService.cs | 4 +- SysManager/SysManager/SysManager.csproj | 6 +-- .../ViewModels/MainWindowViewModel.cs | 2 + .../ViewModels/PrivacyMonitorViewModel.cs | 20 ++++++++ .../ViewModels/SettingsWatchdogViewModel.cs | 22 +++++++- 10 files changed, 177 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b02cfd6..2722093b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 0a6f45dc..6d52b4cb 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/SysManager/SysManager.Tests/CsvExportTests.cs b/SysManager/SysManager.Tests/CsvExportTests.cs index cbee9a36..a786cb0b 100644 --- a/SysManager/SysManager.Tests/CsvExportTests.cs +++ b/SysManager/SysManager.Tests/CsvExportTests.cs @@ -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() { diff --git a/SysManager/SysManager.Tests/PrivacyMonitorViewModelTests.cs b/SysManager/SysManager.Tests/PrivacyMonitorViewModelTests.cs index 925bee63..75f3fee2 100644 --- a/SysManager/SysManager.Tests/PrivacyMonitorViewModelTests.cs +++ b/SysManager/SysManager.Tests/PrivacyMonitorViewModelTests.cs @@ -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(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); + } } diff --git a/SysManager/SysManager.Tests/SettingsWatchdogViewModelTests.cs b/SysManager/SysManager.Tests/SettingsWatchdogViewModelTests.cs index ec81aaa7..877a2b6e 100644 --- a/SysManager/SysManager.Tests/SettingsWatchdogViewModelTests.cs +++ b/SysManager/SysManager.Tests/SettingsWatchdogViewModelTests.cs @@ -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(), Arg.Any>()).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); + } } diff --git a/SysManager/SysManager/Services/SettingsWatchdogService.cs b/SysManager/SysManager/Services/SettingsWatchdogService.cs index d9590b28..35256be2 100644 --- a/SysManager/SysManager/Services/SettingsWatchdogService.cs +++ b/SysManager/SysManager/Services/SettingsWatchdogService.cs @@ -416,7 +416,9 @@ public static string ToCsv(IEnumerable 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(); } diff --git a/SysManager/SysManager/SysManager.csproj b/SysManager/SysManager/SysManager.csproj index 1181e9fb..96a52b37 100644 --- a/SysManager/SysManager/SysManager.csproj +++ b/SysManager/SysManager/SysManager.csproj @@ -13,9 +13,9 @@ $(NoWarn);NU1603;NU1701 false - 1.130.49 - 1.130.49.0 - 1.130.49.0 + 1.130.50 + 1.130.50.0 + 1.130.50.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/MainWindowViewModel.cs b/SysManager/SysManager/ViewModels/MainWindowViewModel.cs index 1551a7ec..bf128520 100644 --- a/SysManager/SysManager/ViewModels/MainWindowViewModel.cs +++ b/SysManager/SysManager/ViewModels/MainWindowViewModel.cs @@ -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; } } diff --git a/SysManager/SysManager/ViewModels/PrivacyMonitorViewModel.cs b/SysManager/SysManager/ViewModels/PrivacyMonitorViewModel.cs index efc72a19..2d86f179 100644 --- a/SysManager/SysManager/ViewModels/PrivacyMonitorViewModel.cs +++ b/SysManager/SysManager/ViewModels/PrivacyMonitorViewModel.cs @@ -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."; + /// + /// True while the tab is on screen. Set by ; becoming visible reads + /// again. + /// + /// + /// 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). + /// + [ObservableProperty] private bool _isActive; + + /// The read the tab started when it was last shown. Internal so a test can await it. + internal Task ShownRefresh { get; private set; } = Task.CompletedTask; + public PrivacyMonitorViewModel(PrivacyMonitorService service) : this(service.ReadAsync) { } /// Test seam: the consent-store read, so a failed one can be supplied without the registry. @@ -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() { diff --git a/SysManager/SysManager/ViewModels/SettingsWatchdogViewModel.cs b/SysManager/SysManager/ViewModels/SettingsWatchdogViewModel.cs index 4df48bc5..1210f62e 100644 --- a/SysManager/SysManager/ViewModels/SettingsWatchdogViewModel.cs +++ b/SysManager/SysManager/ViewModels/SettingsWatchdogViewModel.cs @@ -48,6 +48,17 @@ public sealed partial class SettingsWatchdogViewModel : ViewModelBase [ObservableProperty] private bool _hasDrift; [ObservableProperty] private bool _isElevated; + /// + /// True while the tab is on screen. Set by ; becoming visible looks + /// again. + /// + /// + /// 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). + /// + [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; @@ -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() { @@ -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; } ///