Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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_Returns50()
{
var disks = new List<DiskHealthReport>
{
new() { HealthStatus = "Healthy" },
new() { HealthStatus = "Warning" }
};
Assert.Equal(50, 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