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
2 changes: 2 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ Key services:
- `SystemInfoService` — OS / CPU / RAM / uptime snapshot.
- `TuneUpService` — orchestrates the Quick Tune-Up wizard: temp cleanup,
Recycle Bin, shortcut scan, disk SMART, uptime/RAM checks. Non-destructive.
- `HealthScoreService` — aggregates disk health, RAM, uptime, and battery
wear into a single 0–100 score with color-coded verdict and recommendations.
- `LogService` — Serilog wrapper with rolling file sink.
- `FixedDriveService` — enumerate fixed NTFS/ReFS volumes.
- `DeepCleanupService` — scan-first safe cleanup (vendor caches, gaming
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
flags high uptime (14+ days) and high RAM usage (85%+). Displays a
dismissible summary card with freed space, disk verdicts, and
recommendations. Non-destructive, no admin required. Closes #261.
- **Dashboard — Health Score card** — overall system health gauge (0–100)
combining disk SMART, RAM usage, uptime, and battery wear (on laptops).
Color-coded circular ring with label (Excellent/Good/Fair/Poor) and up
to 3 actionable recommendations. Auto-computes on load and refreshes
with "Scan system". Closes #259.
- **HealthScoreService** — aggregates SystemInfoService, DiskHealthService,
and BatteryService into a weighted health score.
- **IntGreaterThanZeroConverter** — value converter for conditional
visibility when an integer is greater than zero.
- **IDialogService** — abstraction for user confirmation dialogs, replacing
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ long-running operation, so you always know which tab is working.
health, flags high uptime (14+ days) and high RAM usage (85%+). Displays
a summary card with freed space, warnings, and links to relevant tabs.
Non-destructive, no admin required.
- **Health Score** — overall system health gauge (0–100) combining disk
SMART, RAM usage, uptime, and battery wear. Color-coded ring (green /
amber / red) with up to 3 actionable recommendations. Auto-computes on
load and refreshes with "Scan system".

### Updates (for SysManager itself)
- Auto-check on startup against the GitHub Releases API, plus a manual
Expand Down
6 changes: 4 additions & 2 deletions SysManager/SysManager.Tests/DashboardViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ public class DashboardViewModelTests
private static DashboardViewModel NewVm()
{
var sys = new SystemInfoService();
return new DashboardViewModel(sys, new TuneUpService(
new ShortcutCleanerService(), new DiskHealthService(), sys));
var diskHealth = new DiskHealthService();
return new DashboardViewModel(sys,
new TuneUpService(new ShortcutCleanerService(), diskHealth, sys),
new HealthScoreService(sys, diskHealth, new BatteryService()));
}

// ---------- construction & defaults ----------
Expand Down
263 changes: 263 additions & 0 deletions SysManager/SysManager.Tests/HealthScoreServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
// SysManager · HealthScoreServiceTests
// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager
// License: MIT

using SysManager.Models;
using SysManager.Services;

namespace SysManager.Tests;

/// <summary>
/// Unit tests for <see cref="HealthScoreService"/> — validates scoring logic
/// for each component and the aggregation formula.
/// </summary>
public class HealthScoreServiceTests
{
// ---------- construction ----------

[Fact]
public void Constructor_DoesNotThrow()
{
var svc = new HealthScoreService(
new SystemInfoService(), new DiskHealthService(), new BatteryService());
Assert.NotNull(svc);
}

// ---------- ComputeDiskScore ----------

[Fact]
public void ComputeDiskScore_NullDisks_Returns100()
{
Assert.Equal(100, HealthScoreService.ComputeDiskScore(null));
}

[Fact]
public void ComputeDiskScore_EmptyList_Returns100()
{
Assert.Equal(100, HealthScoreService.ComputeDiskScore(Array.Empty<DiskHealthReport>()));
}

[Fact]
public void ComputeDiskScore_AllHealthy_Returns100()
{
var disks = new List<DiskHealthReport>
{
new() { HealthStatus = "Healthy" },
new() { HealthStatus = "Healthy" }
};
Assert.Equal(100, HealthScoreService.ComputeDiskScore(disks));
}

[Fact]
public void ComputeDiskScore_OneWarning_Returns60()
{
var disks = new List<DiskHealthReport>
{
new() { HealthStatus = "Healthy" },
new() { HealthStatus = "Warning" }
};
Assert.Equal(60, HealthScoreService.ComputeDiskScore(disks));
}

[Fact]
public void ComputeDiskScore_OneUnhealthy_Returns20()
{
var disks = new List<DiskHealthReport>
{
new() { HealthStatus = "Unhealthy" }
};
Assert.Equal(20, HealthScoreService.ComputeDiskScore(disks));
}

// ---------- ComputeRamScore ----------

[Fact]
public void ComputeRamScore_NullSnapshot_Returns100()
{
Assert.Equal(100, HealthScoreService.ComputeRamScore(null));
}

[Fact]
public void ComputeRamScore_LowUsage_Returns100()
{
var snapshot = MakeSnapshot(ramUsedPct: 40);
Assert.Equal(100, HealthScoreService.ComputeRamScore(snapshot));
}

[Fact]
public void ComputeRamScore_60Percent_Returns100()
{
var snapshot = MakeSnapshot(ramUsedPct: 60);
Assert.Equal(100, HealthScoreService.ComputeRamScore(snapshot));
}

[Fact]
public void ComputeRamScore_75Percent_Returns75()
{
var snapshot = MakeSnapshot(ramUsedPct: 75);
Assert.Equal(75, HealthScoreService.ComputeRamScore(snapshot));
}

[Fact]
public void ComputeRamScore_90Percent_Returns40()
{
var snapshot = MakeSnapshot(ramUsedPct: 90);
Assert.Equal(40, HealthScoreService.ComputeRamScore(snapshot));
}

[Fact]
public void ComputeRamScore_98Percent_Returns10()
{
var snapshot = MakeSnapshot(ramUsedPct: 98);
Assert.Equal(10, HealthScoreService.ComputeRamScore(snapshot));
}

// ---------- ComputeUptimeScore ----------

[Fact]
public void ComputeUptimeScore_NullSnapshot_Returns100()
{
Assert.Equal(100, HealthScoreService.ComputeUptimeScore(null));
}

[Fact]
public void ComputeUptimeScore_1Day_Returns100()
{
var snapshot = MakeSnapshot(uptimeDays: 1);
Assert.Equal(100, HealthScoreService.ComputeUptimeScore(snapshot));
}

[Fact]
public void ComputeUptimeScore_5Days_Returns90()
{
var snapshot = MakeSnapshot(uptimeDays: 5);
Assert.Equal(90, HealthScoreService.ComputeUptimeScore(snapshot));
}

[Fact]
public void ComputeUptimeScore_10Days_Returns70()
{
var snapshot = MakeSnapshot(uptimeDays: 10);
Assert.Equal(70, HealthScoreService.ComputeUptimeScore(snapshot));
}

[Fact]
public void ComputeUptimeScore_20Days_Returns50()
{
var snapshot = MakeSnapshot(uptimeDays: 20);
Assert.Equal(50, HealthScoreService.ComputeUptimeScore(snapshot));
}

[Fact]
public void ComputeUptimeScore_45Days_Returns15()
{
var snapshot = MakeSnapshot(uptimeDays: 45);
Assert.Equal(15, HealthScoreService.ComputeUptimeScore(snapshot));
}

// ---------- ComputeBatteryScore ----------

[Fact]
public void ComputeBatteryScore_NullBattery_Returns100()
{
Assert.Equal(100, HealthScoreService.ComputeBatteryScore(null));
}

[Fact]
public void ComputeBatteryScore_NoBattery_Returns100()
{
var battery = new BatteryInfo { HasBattery = false };
Assert.Equal(100, HealthScoreService.ComputeBatteryScore(battery));
}

[Fact]
public void ComputeBatteryScore_HealthyBattery_Returns100()
{
var battery = new BatteryInfo
{
HasBattery = true,
DesignCapacityMWh = 50000,
FullChargeCapacityMWh = 48000 // 96% health
};
Assert.Equal(100, HealthScoreService.ComputeBatteryScore(battery));
}

[Fact]
public void ComputeBatteryScore_DegradedBattery_Returns80()
{
var battery = new BatteryInfo
{
HasBattery = true,
DesignCapacityMWh = 50000,
FullChargeCapacityMWh = 35000 // 70% health
};
Assert.Equal(80, HealthScoreService.ComputeBatteryScore(battery));
}

[Fact]
public void ComputeBatteryScore_WornBattery_Returns55()
{
var battery = new BatteryInfo
{
HasBattery = true,
DesignCapacityMWh = 50000,
FullChargeCapacityMWh = 22000 // 44% health
};
Assert.Equal(55, HealthScoreService.ComputeBatteryScore(battery));
}

// ---------- HealthScoreResult model ----------

[Theory]
[InlineData(100, "#22C55E")]
[InlineData(80, "#22C55E")]
[InlineData(79, "#F59E0B")]
[InlineData(50, "#F59E0B")]
[InlineData(49, "#EF4444")]
[InlineData(0, "#EF4444")]
public void HealthScoreResult_ColorHex_MatchesScore(int score, string expectedColor)
{
var result = new HealthScoreResult { Score = score };
Assert.Equal(expectedColor, result.ColorHex);
}

[Theory]
[InlineData(95, "Excellent")]
[InlineData(85, "Good")]
[InlineData(65, "Fair")]
[InlineData(45, "Needs attention")]
[InlineData(20, "Poor")]
public void HealthScoreResult_Label_MatchesScore(int score, string expectedLabel)
{
var result = new HealthScoreResult { Score = score };
Assert.Equal(expectedLabel, result.Label);
}

[Fact]
public void HealthRecommendation_CriticalSeverity_RedColor()
{
var rec = new HealthRecommendation { Message = "Test", Severity = "critical" };
Assert.Equal("#EF4444", rec.ColorHex);
}

[Fact]
public void HealthRecommendation_WarningSeverity_AmberColor()
{
var rec = new HealthRecommendation { Message = "Test", Severity = "warning" };
Assert.Equal("#F59E0B", rec.ColorHex);
}

// ---------- helpers ----------

private static SystemSnapshot MakeSnapshot(double ramUsedPct = 50, int uptimeDays = 1)
{
double totalGB = 16;
double usedGB = totalGB * ramUsedPct / 100;
return new SystemSnapshot(
new OsInfo("Windows 11", "10.0", "22631", TimeSpan.FromDays(uptimeDays), "64-bit"),
new CpuInfo("Test CPU", 8, 16, 3600, 10),
new MemoryInfo(totalGB, totalGB - usedGB, usedGB, ramUsedPct, new List<MemoryModule>()),
new List<DiskInfo>(),
DateTime.Now);
}
}
54 changes: 54 additions & 0 deletions SysManager/SysManager/Models/HealthScoreResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SysManager · HealthScoreResult — aggregated system health score
// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager
// License: MIT

namespace SysManager.Models;

/// <summary>
/// Aggregated health score (0–100) combining disk health, RAM usage,
/// uptime, and battery wear. Higher is better.
/// </summary>
public sealed class HealthScoreResult
{
/// <summary>Overall score 0–100 (100 = perfect health).</summary>
public int Score { get; init; }

/// <summary>Color hex for the gauge arc.</summary>
public string ColorHex => Score switch
{
>= 80 => "#22C55E", // green
>= 50 => "#F59E0B", // amber
_ => "#EF4444" // red
};

/// <summary>Human-readable label for the score.</summary>
public string Label => Score switch
{
>= 90 => "Excellent",
>= 80 => "Good",
>= 60 => "Fair",
>= 40 => "Needs attention",
_ => "Poor"
};

/// <summary>Top recommendations (max 3).</summary>
public IReadOnlyList<HealthRecommendation> Recommendations { get; init; }
= Array.Empty<HealthRecommendation>();

/// <summary>Individual component scores for breakdown display.</summary>
public int DiskScore { get; init; } = 100;
public int RamScore { get; init; } = 100;
public int UptimeScore { get; init; } = 100;
public int BatteryScore { get; init; } = 100;
public bool HasBattery { get; init; }
}

/// <summary>A single health recommendation shown below the gauge.</summary>
public sealed class HealthRecommendation
{
public required string Message { get; init; }
public required string Severity { get; init; } // "warning" or "critical"

public string IconGlyph => Severity == "critical" ? "\uE783" : "\uE7BA";
public string ColorHex => Severity == "critical" ? "#EF4444" : "#F59E0B";
}
Loading
Loading