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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ 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.14] - 2026-09-04

Double-clicking a cell in the App Updates or Windows Update list used to put a cursor in it, as if you could
change what it said. You could type — and the app threw the change away. Those lists are reports now, and
behave like it.

### Fixed
- **Report cells no longer pretend to be editable.** Ten cells across App Updates and Windows Update accepted
typing that went nowhere. One of them was the app's internal Id for a package, which is what gets handed to
Windows' installer — retyping it turned a row that would have updated into a row that reports an error, for
no reason the user could see. The tick boxes for choosing rows are untouched and still work.

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


/// <summary>
/// A text column in a report grid must not accept typing.
/// </summary>
/// <remarks>
/// Ten cells across App Updates and Windows Update entered edit mode on a double-click. Typing there
/// changed the in-memory row and nothing else, so the app appeared to accept an edit it silently discarded
/// — and one of them was App Updates' <c>Id</c>, the value <c>WingetService.UpgradeAsync</c> builds
/// <c>winget upgrade --id "…"</c> from, which turns a working row into an error row.
/// <para>Per-column, not grid-level. Both grids carry a <c>DataGridCheckBoxColumn</c> for row selection,
/// and <c>DataGrid.IsReadOnly="True"</c> renders those checkboxes untickable — it would break "Upgrade
/// selected" outright. The 20 views that DO set it grid-wide have no checkbox column, which is why they
/// can. Reading the omission as forgetfulness and setting it globally would have been the wrong fix.</para>
/// <para>Nothing reaches a command line through an edited cell: <c>WingetId.IsValid</c> rejects the value
/// and <c>AppUpdatesViewModel</c> catches the <c>ArgumentException</c> per row. This is a UI-honesty rule,
/// not a security one.</para>
/// </remarks>
[Fact]
public void EveryReportTextColumn_IsReadOnly()
{
// view -> the column Header allowed to be editable, and why.
var editors = new Dictionary<string, string>(StringComparer.Ordinal)
{
["EnvironmentVariablesView.xaml"] =
"Value — this tab is an editor, not a report: Apply / Discard / Restore backup sit beside the "
+ "grid and UpdateSourceTrigger=PropertyChanged carries each keystroke to the view-model",
};

var viewsDir = Path.Combine(FindAppProjectDir(), "Views");
var columnsChecked = 0;
var typeable = new List<string>();

foreach (var file in Directory.GetFiles(viewsDir, "*.xaml"))
{
var name = Path.GetFileName(file);
var text = File.ReadAllText(file);

// Every grid in the file must be read-only for the grid-level form to count for any column in it.
var grids = DataGridOpeningTag().Matches(text).Select(m => m.Value).ToList();
if (grids.Count == 0) continue;
var gridReadOnly = grids.TrueForAll(g => g.Contains("IsReadOnly=\"True\"", StringComparison.Ordinal));

foreach (var column in ReportTextColumn().Matches(text).Select(m => m.Value))
{
columnsChecked++;
if (gridReadOnly || column.Contains("IsReadOnly", StringComparison.Ordinal)) continue;

var header = ColumnHeader().Match(column).Groups["header"].Value;
if (editors.TryGetValue(name, out var allowed)
&& allowed.StartsWith(header + " ", StringComparison.Ordinal)) continue;

typeable.Add($"{name}: {header}");
}
}

// Vacuity floor: 125 text columns across the views today, measured with this exact pattern. The
// count matters twice over — a first pass at this used a pattern that also matched
// <DataGridTextColumn.CellStyle>, a property element rather than a column. There are 43 of those, so
// the population read as 168 and every per-view "typeable cells" number was inflated with it.
Assert.True(columnsChecked >= 118,
$"only {columnsChecked} report text columns were parsed out of 125 measured — the pattern no "
+ "longer matches the column shape, so this guard proves nothing.");

Assert.True(typeable.Count == 0,
"these report cells enter edit mode on a double-click, and the edit goes nowhere. Add "
+ "IsReadOnly=\"True\" to the column — NOT to the DataGrid, which would also stop the user "
+ "ticking a DataGridCheckBoxColumn. If the cell is genuinely meant to be edited, name it in the "
+ "exception list in this test WITH its reason:\n " + string.Join("\n ", typeable));
}

/// <summary>A <c>DataGrid</c> opening tag.</summary>
[GeneratedRegex(@"<DataGrid(?![.\w])[^>]*?>", RegexOptions.Singleline)]
private static partial Regex DataGridOpeningTag();

/// <summary>
/// A <c>DataGridTextColumn</c> element. The negative lookahead keeps
/// <c>&lt;DataGridTextColumn.CellStyle&gt;</c> — a property element, not a column — out of the match.
/// </summary>
[GeneratedRegex(@"<DataGridTextColumn(?![.\w])[^>]*?/?>", RegexOptions.Singleline)]
private static partial Regex ReportTextColumn();

/// <summary>A column's <c>Header</c> attribute value.</summary>
[GeneratedRegex(@"Header=""(?<header>[^""]*)""")]
private static partial Regex ColumnHeader();

/// <summary>
/// A view-model that tracks its own "running" state must forward it to <c>IsBusy</c>.
/// </summary>
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.13</Version>
<FileVersion>1.76.13.0</FileVersion>
<AssemblyVersion>1.76.13.0</AssemblyVersion>
<Version>1.76.14</Version>
<FileVersion>1.76.14.0</FileVersion>
<AssemblyVersion>1.76.14.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
12 changes: 6 additions & 6 deletions SysManager/SysManager/Views/AppUpdatesView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,12 @@
</Style>
</DataGridCheckBoxColumn.EditingElementStyle>
</DataGridCheckBoxColumn>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="2*" MinWidth="120" SortMemberPath="Name"/>
<DataGridTextColumn Header="Id" Binding="{Binding Id}" Width="2*" MinWidth="120" SortMemberPath="Id"/>
<DataGridTextColumn Header="Current" Binding="{Binding CurrentVersion}" Width="*" MinWidth="80" SortMemberPath="CurrentVersion"/>
<DataGridTextColumn Header="Available" Binding="{Binding AvailableVersion}" Width="*" MinWidth="80" SortMemberPath="AvailableVersion"/>
<DataGridTextColumn Header="Source" Binding="{Binding Source}" Width="80" MinWidth="60" SortMemberPath="Source"/>
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="*" MinWidth="80" SortMemberPath="Status"/>
<DataGridTextColumn IsReadOnly="True" Header="Name" Binding="{Binding Name}" Width="2*" MinWidth="120" SortMemberPath="Name"/>
<DataGridTextColumn IsReadOnly="True" Header="Id" Binding="{Binding Id}" Width="2*" MinWidth="120" SortMemberPath="Id"/>
<DataGridTextColumn IsReadOnly="True" Header="Current" Binding="{Binding CurrentVersion}" Width="*" MinWidth="80" SortMemberPath="CurrentVersion"/>
<DataGridTextColumn IsReadOnly="True" Header="Available" Binding="{Binding AvailableVersion}" Width="*" MinWidth="80" SortMemberPath="AvailableVersion"/>
<DataGridTextColumn IsReadOnly="True" Header="Source" Binding="{Binding Source}" Width="80" MinWidth="60" SortMemberPath="Source"/>
<DataGridTextColumn IsReadOnly="True" Header="Status" Binding="{Binding Status}" Width="*" MinWidth="80" SortMemberPath="Status"/>
</DataGrid.Columns>
</DataGrid>

Expand Down
5 changes: 5 additions & 0 deletions SysManager/SysManager/Views/EnvironmentVariablesView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" IsReadOnly="True" Width="180"/>
<DataGridTextColumn Header="Scope" Binding="{Binding ScopeLabel}" IsReadOnly="True" Width="70"/>
<!-- The one editable report cell in the app, and deliberately so: this tab IS an
editor, with Apply / Discard / Restore backup beside the grid, and
UpdateSourceTrigger=PropertyChanged is what carries each keystroke to the
view-model for Apply to write. A uniformity sweep must not make this read-only;
EveryReportTextColumn_IsReadOnly names it as the exception. -->
<DataGridTextColumn Header="Value" Binding="{Binding Value, UpdateSourceTrigger=PropertyChanged}" Width="*">
<!-- Trim the *display* of a long value (e.g. a full PATH) against the
Remove button and expose it on hover; the edit box still shows the
Expand Down
8 changes: 4 additions & 4 deletions SysManager/SysManager/Views/WindowsUpdateView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>

<DataGridTextColumn Binding="{Binding KB}" Width="100" MinWidth="70"
<DataGridTextColumn IsReadOnly="True" Binding="{Binding KB}" Width="100" MinWidth="70"
SortMemberPath="KB">
<DataGridTextColumn.Header>
<TextBlock Text="KB"
Expand All @@ -240,7 +240,7 @@
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>

<DataGridTextColumn Header="Size" Binding="{Binding Size}" Width="80" MinWidth="60"
<DataGridTextColumn IsReadOnly="True" Header="Size" Binding="{Binding Size}" Width="80" MinWidth="60"
SortMemberPath="Size">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
Expand All @@ -251,7 +251,7 @@
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>

<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="200" MinWidth="120"
<DataGridTextColumn IsReadOnly="True" Header="Status" Binding="{Binding Status}" Width="200" MinWidth="120"
SortMemberPath="Status">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
Expand All @@ -263,7 +263,7 @@
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>

<DataGridTextColumn Header="Date" Binding="{Binding DateDisplay}" Width="100" MinWidth="80"
<DataGridTextColumn IsReadOnly="True" Header="Date" Binding="{Binding DateDisplay}" Width="100" MinWidth="80"
SortMemberPath="Date">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
Expand Down