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: 10 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed
- **Startup Manager columns** — reduced fixed widths to prevent last column overflow.
- **Startup Manager icons** — use extracted executable path for more accurate icon resolution.
- **Windows Update live output** — increased MinHeight/MaxHeight for better visibility.
## [1.10.0] - 2026-05-26

### Added
- **Safety ratings on Services** — each service shows Safe/Caution/Critical badge with
description tooltip. Filter chips in toolbar to show only safe-to-disable services.
- **Safety ratings on Windows Features** — same badge system as Services.
- **Curated safety database** — 50+ services and 20+ features with researched safety
levels and human-readable explanations.
- **Startup Manager hide system** — toggle to filter out Windows/Microsoft system entries.
- **Filter chip styles** — reusable green/amber/red radio pill components.

## [1.9.0] - 2026-05-26

Expand Down
30 changes: 15 additions & 15 deletions SysManager/SysManager.Tests/ServicesViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ public class ServicesViewModelTests
{
private static readonly List<ServiceEntry> TestServices = new()
{
new() { Name = "wuauserv", DisplayName = "Windows Update", Description = "Manages Windows updates", Status = "Running", StartType = "Automatic", Recommendation = "keep-enabled" },
new() { Name = "Spooler", DisplayName = "Print Spooler", Description = "Manages print jobs", Status = "Running", StartType = "Automatic", Recommendation = "safe-to-disable" },
new() { Name = "XboxGipSvc", DisplayName = "Xbox Accessory Management", Description = "Manages Xbox accessories", Status = "Stopped", StartType = "Manual", Recommendation = "safe-to-disable" },
new() { Name = "WSearch", DisplayName = "Windows Search", Description = "Provides content indexing", Status = "Running", StartType = "Automatic", Recommendation = "advanced" },
new() { Name = "BITS", DisplayName = "Background Intelligent Transfer", Description = "Transfers files in background", Status = "Stopped", StartType = "Manual", Recommendation = "keep-enabled" },
new() { Name = "wuauserv", DisplayName = "Windows Update", Description = "Manages Windows updates", Status = "Running", StartType = "Automatic", Recommendation = "keep-enabled", SafetyLevel = Models.SafetyLevel.Caution },
new() { Name = "Spooler", DisplayName = "Print Spooler", Description = "Manages print jobs", Status = "Running", StartType = "Automatic", Recommendation = "safe-to-disable", SafetyLevel = Models.SafetyLevel.Caution },
new() { Name = "XboxGipSvc", DisplayName = "Xbox Accessory Management", Description = "Manages Xbox accessories", Status = "Stopped", StartType = "Manual", Recommendation = "safe-to-disable", SafetyLevel = Models.SafetyLevel.Safe },
new() { Name = "WSearch", DisplayName = "Windows Search", Description = "Provides content indexing", Status = "Running", StartType = "Automatic", Recommendation = "advanced", SafetyLevel = Models.SafetyLevel.Caution },
new() { Name = "BITS", DisplayName = "Background Intelligent Transfer", Description = "Transfers files in background", Status = "Stopped", StartType = "Manual", Recommendation = "keep-enabled", SafetyLevel = Models.SafetyLevel.Critical },
};

private static ServicesViewModel CreateWithData(List<ServiceEntry>? services = null)
Expand Down Expand Up @@ -52,8 +52,9 @@ public void Constructor_FilterOptions_ContainsExpected()
Assert.Contains("All", vm.FilterOptions);
Assert.Contains("Running", vm.FilterOptions);
Assert.Contains("Stopped", vm.FilterOptions);
Assert.Contains("Safe to disable", vm.FilterOptions);
Assert.Contains("Advanced", vm.FilterOptions);
Assert.Contains("Safe", vm.FilterOptions);
Assert.Contains("Caution", vm.FilterOptions);
Assert.Contains("Critical", vm.FilterOptions);
}

[Fact]
Expand Down Expand Up @@ -111,21 +112,20 @@ public void ApplyFilter_Stopped_ShowsOnlyStopped()
}

[Fact]
public void ApplyFilter_SafeToDisable_ShowsOnlySafe()
public void ApplyFilter_SafeLevel_ShowsOnlySafe()
{
var vm = CreateWithData();
vm.SelectedFilter = "Safe to disable";
Assert.All(vm.Services, s => Assert.Equal("safe-to-disable", s.Recommendation));
Assert.Equal(2, vm.Services.Count);
vm.SelectedFilter = "Safe";
Assert.All(vm.Services, s => Assert.Equal(Models.SafetyLevel.Safe, s.SafetyLevel));
Assert.Single(vm.Services);
}

[Fact]
public void ApplyFilter_Advanced_ShowsOnlyAdvanced()
public void ApplyFilter_Safe_ShowsOnlySafe()
{
var vm = CreateWithData();
vm.SelectedFilter = "Advanced";
Assert.All(vm.Services, s => Assert.Equal("advanced", s.Recommendation));
Assert.Single(vm.Services);
vm.SelectedFilter = "Safe";
Assert.All(vm.Services, s => Assert.Equal(Models.SafetyLevel.Safe, s.SafetyLevel));
}

// ── ApplyFilter: text filter ──
Expand Down
89 changes: 89 additions & 0 deletions SysManager/SysManager/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
<h:HexToBrushConverter x:Key="HexBrush"/>
<h:OutputKindToBrushConverter x:Key="KindBrush"/>
<h:ProcessStatusToBrushConverter x:Key="StatusBrush"/>
<h:SafetyLevelToBrushConverter x:Key="SafetyBrush"/>
<h:SafetyLevelToBackgroundConverter x:Key="SafetyBg"/>
<h:SafetyLevelToTextConverter x:Key="SafetyText"/>
<h:IntGreaterThanZeroConverter x:Key="GreaterThanZero"/>
<h:EqualityConverter x:Key="IsEqual"/>

Expand Down Expand Up @@ -273,6 +276,92 @@
</Setter>
</Style>

<!-- ============================================================ -->
<!-- Filter Chips — safety level radio buttons -->
<!-- ============================================================ -->
<Style x:Key="FilterChip" TargetType="RadioButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border x:Name="Bd" CornerRadius="999" Padding="8,4"
Background="#1522C55E" BorderBrush="#3022C55E" BorderThickness="1"
Cursor="Hand">
<StackPanel Orientation="Horizontal">
<Ellipse x:Name="Dot" Width="6" Height="6" Fill="#22C55E" Margin="0,0,5,0"/>
<ContentPresenter VerticalAlignment="Center"/>
</StackPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#3022C55E"/>
<Setter TargetName="Bd" Property="BorderBrush" Value="#6022C55E"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#186366F1"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="#4ADE80"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="FontWeight" Value="Medium"/>
</Style>

<Style x:Key="FilterChipCaution" TargetType="RadioButton" BasedOn="{StaticResource FilterChip}">
<Setter Property="Foreground" Value="#FBBF24"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border x:Name="Bd" CornerRadius="999" Padding="8,4"
Background="#15F59E0B" BorderBrush="#30F59E0B" BorderThickness="1"
Cursor="Hand">
<StackPanel Orientation="Horizontal">
<Ellipse x:Name="Dot" Width="6" Height="6" Fill="#F59E0B" Margin="0,0,5,0"/>
<ContentPresenter VerticalAlignment="Center"/>
</StackPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#30F59E0B"/>
<Setter TargetName="Bd" Property="BorderBrush" Value="#60F59E0B"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#186366F1"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Style x:Key="FilterChipCritical" TargetType="RadioButton" BasedOn="{StaticResource FilterChip}">
<Setter Property="Foreground" Value="#F87171"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border x:Name="Bd" CornerRadius="999" Padding="8,4"
Background="#15EF4444" BorderBrush="#30EF4444" BorderThickness="1"
Cursor="Hand">
<StackPanel Orientation="Horizontal">
<Ellipse x:Name="Dot" Width="6" Height="6" Fill="#EF4444" Margin="0,0,5,0"/>
<ContentPresenter VerticalAlignment="Center"/>
</StackPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#30EF4444"/>
<Setter TargetName="Bd" Property="BorderBrush" Value="#60EF4444"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#186366F1"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<!-- Admin: golden glass button for elevation requests -->
<Style x:Key="AdminButton" TargetType="Button" BasedOn="{StaticResource ButtonBase}">
<Setter Property="Foreground" Value="#FCD34D"/>
Expand Down
73 changes: 73 additions & 0 deletions SysManager/SysManager/Helpers/ValueConverters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,76 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}

public sealed class SafetyLevelToBrushConverter : IValueConverter
{
private static readonly SolidColorBrush SafeBrush = new(Color.FromRgb(0x4A, 0xDE, 0x80));
private static readonly SolidColorBrush CautionBrush = new(Color.FromRgb(0xFB, 0xBF, 0x24));
private static readonly SolidColorBrush CriticalBrush = new(Color.FromRgb(0xF8, 0x71, 0x71));

static SafetyLevelToBrushConverter()
{
SafeBrush.Freeze();
CautionBrush.Freeze();
CriticalBrush.Freeze();
}

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is SafetyLevel level ? level switch
{
SafetyLevel.Safe => SafeBrush,
SafetyLevel.Caution => CautionBrush,
SafetyLevel.Critical => CriticalBrush,
_ => CriticalBrush
} : CriticalBrush;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}

public sealed class SafetyLevelToBackgroundConverter : IValueConverter
{
private static readonly SolidColorBrush SafeBg = new(Color.FromArgb(0x20, 0x22, 0xC5, 0x5E));
private static readonly SolidColorBrush CautionBg = new(Color.FromArgb(0x20, 0xF5, 0x9E, 0x0B));
private static readonly SolidColorBrush CriticalBg = new(Color.FromArgb(0x20, 0xEF, 0x44, 0x44));

static SafetyLevelToBackgroundConverter()
{
SafeBg.Freeze();
CautionBg.Freeze();
CriticalBg.Freeze();
}

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is SafetyLevel level ? level switch
{
SafetyLevel.Safe => SafeBg,
SafetyLevel.Caution => CautionBg,
SafetyLevel.Critical => CriticalBg,
_ => CriticalBg
} : CriticalBg;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}

public sealed class SafetyLevelToTextConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is SafetyLevel level ? level switch
{
SafetyLevel.Safe => "Safe",
SafetyLevel.Caution => "Caution",
SafetyLevel.Critical => "Critical",
_ => "Critical"
} : "Critical";
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
12 changes: 12 additions & 0 deletions SysManager/SysManager/Models/SafetyLevel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SysManager · SafetyLevel
// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager
// License: MIT

namespace SysManager.Models;

public enum SafetyLevel
{
Safe,
Caution,
Critical
}
3 changes: 3 additions & 0 deletions SysManager/SysManager/Models/ServiceEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ public sealed partial class ServiceEntry : ObservableObject

/// <summary>Short explanation of what this service does and why the recommendation.</summary>
public string RecommendationReason { get; init; } = "";

public SafetyLevel SafetyLevel { get; init; } = SafetyLevel.Critical;
public string SafetyDescription { get; init; } = "";
}
3 changes: 3 additions & 0 deletions SysManager/SysManager/Models/WindowsFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public sealed partial class WindowsFeature : ObservableObject
[ObservableProperty] private string _category = "Other";
[ObservableProperty] private string _status = "";

public SafetyLevel SafetyLevel { get; init; } = SafetyLevel.Caution;
public string SafetyDescription { get; init; } = "";

/// <summary>Category assignment based on feature name patterns.</summary>
public static string CategorizeFeature(string featureName)
{
Expand Down
Loading
Loading