Files
Scripting/Powershell/Invoke-DiskSpaceReport.ps1
DistractADD c3cc6eb225 Add Powershell/Invoke-DiskSpaceReport.ps1
# Basic — drops HTML on Desktop
.\Invoke-DiskSpaceReport.ps1

# With client name in the header
.\Invoke-DiskSpaceReport.ps1 -ClientName "Department of X"

# Custom output path
.\Invoke-DiskSpaceReport.ps1 -OutputPath "C:\Reports" -ClientName "QPS"
2026-04-23 11:39:22 +10:00

837 lines
31 KiB
PowerShell

#Requires -RunAsAdministrator
<#
.SYNOPSIS
C: Drive Disk Space Report — generates a self-contained HTML report for stakeholders.
.DESCRIPTION
Scans the C: drive across all common space consumers and produces a polished,
printable HTML report. No cleanup is performed. Safe to run at any time.
.NOTES
Supports: Windows Server 2016, 2019, 2022, 2025
Run as: Administrator
Output: HTML file on the Desktop (or path specified by -OutputPath)
Author: SBCIT
#>
param(
[string]$OutputPath = "$env:USERPROFILE\Desktop",
[string]$ClientName = ""
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'SilentlyContinue'
# ─────────────────────────────────────────────
# HELPERS
# ─────────────────────────────────────────────
function Format-Size {
param([long]$Bytes)
if ($Bytes -ge 1GB) { return "{0:N2} GB" -f ($Bytes / 1GB) }
if ($Bytes -ge 1MB) { return "{0:N2} MB" -f ($Bytes / 1MB) }
if ($Bytes -ge 1KB) { return "{0:N2} KB" -f ($Bytes / 1KB) }
return "$Bytes B"
}
function Get-FolderSize {
param([string]$Path)
if (-not (Test-Path $Path)) { return 0 }
try {
(Get-ChildItem -Path $Path -Recurse -Force -File -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum).Sum
} catch { 0 }
}
function Get-RecycleBinSize {
$total = 0
try {
$shell = New-Object -ComObject Shell.Application
$recycle = $shell.Namespace(0xA)
foreach ($item in $recycle.Items()) { try { $total += $item.Size } catch {} }
} catch {}
return $total
}
function Get-SeverityClass {
param([long]$Bytes)
if ($Bytes -ge 1GB) { return "sev-critical" }
if ($Bytes -ge 200MB) { return "sev-warning" }
if ($Bytes -ge 10MB) { return "sev-minor" }
return "sev-ok"
}
function Get-SeverityLabel {
param([long]$Bytes)
if ($Bytes -ge 1GB) { return "High" }
if ($Bytes -ge 200MB) { return "Medium" }
if ($Bytes -ge 10MB) { return "Low" }
return "Minimal"
}
# ─────────────────────────────────────────────
# CONSOLE PROGRESS
# ─────────────────────────────────────────────
Write-Host ""
Write-Host " SBCIT // Disk Space Report Generator" -ForegroundColor DarkCyan
Write-Host " Scanning $env:COMPUTERNAME — please wait...`n" -ForegroundColor Gray
# ─────────────────────────────────────────────
# DRIVE STATE
# ─────────────────────────────────────────────
$drive = Get-PSDrive C
$driveTotal = $drive.Used + $drive.Free
$driveFree = $drive.Free
$driveUsed = $drive.Used
$freePct = [math]::Round(($driveFree / $driveTotal) * 100, 1)
$usedPct = [math]::Round(($driveUsed / $driveTotal) * 100, 1)
$totalGB = [math]::Round($driveTotal / 1GB, 2)
$usedGB = [math]::Round($driveUsed / 1GB, 2)
$freeGB = [math]::Round($driveFree / 1GB, 2)
$driveStatus = if ($freePct -lt 10) { "Critical" } elseif ($freePct -lt 20) { "Low" } else { "Healthy" }
$driveStatusClass = if ($freePct -lt 10) { "sev-critical" } elseif ($freePct -lt 20) { "sev-warning" } else { "sev-ok" }
# OS Version
$osInfo = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
$osName = if ($osInfo) { $osInfo.Caption } else { "Windows Server" }
$osBuild = if ($osInfo) { "Build $($osInfo.BuildNumber)" } else { "" }
# Uptime
$uptime = ""
if ($osInfo -and $osInfo.LastBootUpTime) {
$span = New-TimeSpan -Start $osInfo.LastBootUpTime -End (Get-Date)
$uptime = "$($span.Days)d $($span.Hours)h $($span.Minutes)m"
}
# ─────────────────────────────────────────────
# SCAN
# ─────────────────────────────────────────────
$findings = [System.Collections.Generic.List[PSCustomObject]]::new()
function Add-Finding {
param(
[string]$Category,
[string]$Label,
[string]$Path,
[long]$Size,
[string]$Description,
[string]$Note = ""
)
$findings.Add([PSCustomObject]@{
Category = $Category
Label = $Label
Path = $Path
Size = $Size
Description = $Description
Note = $Note
})
}
# Temp Files
Add-Finding "Temporary Files" "Windows Temp Folder" "C:\Windows\Temp" (Get-FolderSize "C:\Windows\Temp") "System-wide temporary files created by Windows and applications." ""
Add-Finding "Temporary Files" "System Temp ($env:TEMP)" $env:TEMP (Get-FolderSize $env:TEMP) "Temporary files for the current user session." ""
Add-Finding "Temporary Files" "User AppData Temp" "C:\Users\$env:USERNAME\AppData\Local\Temp" (Get-FolderSize "C:\Users\$env:USERNAME\AppData\Local\Temp") "Per-user application temporary data." ""
# Windows Update
Add-Finding "Windows Update" "Update Download Cache" "C:\Windows\SoftwareDistribution\Download" (Get-FolderSize "C:\Windows\SoftwareDistribution\Download") "Cached Windows Update downloads. Can be safely cleared when no updates are pending." ""
Add-Finding "Windows Update" "WinSxS Component Backups" "C:\Windows\WinSxS\Backup" (Get-FolderSize "C:\Windows\WinSxS\Backup") "Old component backups retained after Windows updates." ""
# System Logs
$cbsSize = Get-FolderSize "C:\Windows\Logs\CBS"
Add-Finding "System Logs" "Component-Based Servicing Logs" "C:\Windows\Logs\CBS" $cbsSize "Logs generated during Windows component servicing and updates." ""
$evtxSize = 0
try {
$evtxSize = (Get-ChildItem "C:\Windows\System32\winevt\Logs" -Filter "*.evtx" -File -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum).Sum
} catch {}
Add-Finding "System Logs" "Windows Event Logs" "C:\Windows\System32\winevt\Logs" $evtxSize "Application, System, and Security event logs." "Backed up to Zipline before clearing."
# Error Reporting
Add-Finding "Error Reporting" "WER Report Queue" "C:\ProgramData\Microsoft\Windows\WER\ReportQueue" (Get-FolderSize "C:\ProgramData\Microsoft\Windows\WER\ReportQueue") "Pending Windows Error Reports awaiting upload to Microsoft." ""
Add-Finding "Error Reporting" "WER Report Archive" "C:\ProgramData\Microsoft\Windows\WER\ReportArchive" (Get-FolderSize "C:\ProgramData\Microsoft\Windows\WER\ReportArchive") "Previously sent Windows Error Reports retained locally." ""
Add-Finding "Error Reporting" "User Error Reports" "C:\Users\$env:USERNAME\AppData\Local\Microsoft\Windows\WER" (Get-FolderSize "C:\Users\$env:USERNAME\AppData\Local\Microsoft\Windows\WER") "Per-user application crash reports." ""
# Crash Dumps
$miniDump = Get-FolderSize "C:\Windows\Minidump"
$memDump = if (Test-Path "C:\Windows\MEMORY.DMP") { (Get-Item "C:\Windows\MEMORY.DMP").Length } else { 0 }
Add-Finding "Crash Dumps" "Minidump Files" "C:\Windows\Minidump" $miniDump "Small memory dumps generated when the system encounters a critical error." ""
Add-Finding "Crash Dumps" "Full Memory Dump" "C:\Windows\MEMORY.DMP" $memDump "Complete RAM snapshot from last system crash. Large file — only present after a BSOD." ""
# Delivery Optimisation
Add-Finding "Delivery Optimisation" "DO Peer Cache" "C:\Windows\SoftwareDistribution\DeliveryOptimization" (Get-FolderSize "C:\Windows\SoftwareDistribution\DeliveryOptimization") "Windows Update peer-to-peer delivery cache." ""
Add-Finding "Delivery Optimisation" "DO Network Service Cache" "C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization" (Get-FolderSize "C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization") "Delivery Optimisation metadata cache." ""
# IIS Logs
foreach ($iisPath in @("C:\inetpub\logs\LogFiles","C:\Windows\System32\LogFiles\W3SVC1")) {
$sz = Get-FolderSize $iisPath
if ($sz -gt 0) {
Add-Finding "IIS Web Server Logs" "IIS Logs ($iisPath)" $iisPath $sz "Internet Information Services access and error logs." "Cleanup removes files older than 30 days."
}
}
# Recycle Bin
Add-Finding "Recycle Bin" "C: Recycle Bin" 'C:\$Recycle.Bin' (Get-RecycleBinSize) "Files deleted by users but not yet permanently removed." ""
# Cache
Add-Finding "Cache" "Thumbnail Cache" "C:\Users\$env:USERNAME\AppData\Local\Microsoft\Windows\Explorer" (Get-FolderSize "C:\Users\$env:USERNAME\AppData\Local\Microsoft\Windows\Explorer") "Cached image thumbnails. Rebuilds automatically when browsing folders." ""
Add-Finding "Cache" "Font Cache" "C:\Windows\ServiceProfiles\LocalService\AppData\Local\FontCache" (Get-FolderSize "C:\Windows\ServiceProfiles\LocalService\AppData\Local\FontCache") "Font rendering cache. Rebuilds automatically on next boot." ""
# Installer Cache
$installerCache = Get-FolderSize "C:\Windows\Installer\`$PatchCache`$"
Add-Finding "Installer Cache" "MSI Patch Cache" "C:\Windows\Installer\`$PatchCache`$" $installerCache "Cached MSI installer patches. Clearing may affect application repair." "⚠ Verify before removing."
# Old Profiles
$systemProfiles = @('Administrator','Default','Default User','Public','All Users','LocalService','NetworkService','systemprofile')
$oldProfiles = Get-ChildItem "C:\Users" -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -notin $systemProfiles -and $_.Name -ne $env:USERNAME }
foreach ($prof in $oldProfiles) {
$sz = Get-FolderSize $prof.FullName
Add-Finding "User Profiles" "Profile: $($prof.Name)" $prof.FullName $sz "User profile folder for an account that may no longer be active." "⚠ Verify account status before removing."
}
# Legacy
Add-Finding "Legacy" "Downloaded Program Files" "C:\Windows\Downloaded Program Files" (Get-FolderSize "C:\Windows\Downloaded Program Files") "Legacy ActiveX and Java components. Generally safe to clear on modern servers." ""
# Filter zero-size findings
$findings = $findings | Where-Object { $_.Size -gt 0 }
# ─────────────────────────────────────────────
# DERIVED STATS
# ─────────────────────────────────────────────
$totalRecoverable = ($findings | Measure-Object -Property Size -Sum).Sum
$recoverablePct = if ($driveTotal -gt 0) { [math]::Round(($totalRecoverable / $driveTotal) * 100, 1) } else { 0 }
$projectedFreeGB = [math]::Round(($driveFree + $totalRecoverable) / 1GB, 2)
$projectedFreePct = [math]::Round((($driveFree + $totalRecoverable) / $driveTotal) * 100, 1)
$categoryGroups = $findings | Group-Object Category | Sort-Object { ($_.Group | Measure-Object -Property Size -Sum).Sum } -Descending
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$fileStamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$reportFile = Join-Path $OutputPath "$($env:COMPUTERNAME)_DiskReport_$fileStamp.html"
$reportTitle = if ($ClientName) { "$ClientName$($env:COMPUTERNAME)" } else { $env:COMPUTERNAME }
# ─────────────────────────────────────────────
# BUILD HTML
# ─────────────────────────────────────────────
$rowsHtml = [System.Text.StringBuilder]::new()
foreach ($group in $categoryGroups) {
$groupTotal = ($group.Group | Measure-Object -Property Size -Sum).Sum
$isFirst = $true
$rowCount = $group.Group.Count
foreach ($item in ($group.Group | Sort-Object Size -Descending)) {
$sev = Get-SeverityClass $item.Size
$sevLabel = Get-SeverityLabel $item.Size
$sizeStr = Format-Size $item.Size
$noteHtml = if ($item.Note) { "<span class='note'>$([System.Web.HttpUtility]::HtmlEncode($item.Note))</span>" } else { "" }
if ($isFirst) {
$catCell = "<td class='cat-cell' rowspan='$rowCount'>$([System.Web.HttpUtility]::HtmlEncode($group.Name))<span class='cat-total'>$(Format-Size $groupTotal)</span></td>"
$isFirst = $false
} else {
$catCell = ""
}
[void]$rowsHtml.AppendLine("
<tr>
$catCell
<td>$([System.Web.HttpUtility]::HtmlEncode($item.Label))<br><span class='path'>$([System.Web.HttpUtility]::HtmlEncode($item.Path))</span></td>
<td class='desc-cell'>$([System.Web.HttpUtility]::HtmlEncode($item.Description)) $noteHtml</td>
<td class='size-cell $sev'>$sizeStr</td>
<td class='sev-badge-cell'><span class='badge $sev'>$sevLabel</span></td>
</tr>")
}
}
$barUsed = [math]::Round($usedPct)
$barUsedColor = if ($usedPct -gt 90) { "#e05252" } elseif ($usedPct -gt 75) { "#e09a3a" } else { "#4a7fb5" }
$html = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Disk Space Report $reportTitle</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap');
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f4f5f7;
--surface: #ffffff;
--border: #dde1e9;
--text: #1a2033;
--text-muted: #6b7591;
--accent: #1a4f8a;
--accent-lt: #e8eff8;
--red: #c0392b;
--red-lt: #fdecea;
--amber: #b7621a;
--amber-lt: #fef3e2;
--blue: #1a4f8a;
--blue-lt: #e8eff8;
--green: #1e6b45;
--green-lt: #e6f5ed;
--mono: 'IBM Plex Mono', monospace;
--sans: 'IBM Plex Sans', system-ui, sans-serif;
}
body {
font-family: var(--sans);
background: var(--bg);
color: var(--text);
font-size: 14px;
line-height: 1.6;
}
.page-wrap {
max-width: 1100px;
margin: 0 auto;
padding: 40px 32px 60px;
}
/* HEADER */
.report-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 36px;
padding-bottom: 28px;
border-bottom: 2px solid var(--accent);
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand-logo {
width: 44px; height: 44px;
background: var(--accent);
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
color: #fff;
font-family: var(--mono);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.03em;
text-align: center;
line-height: 1.3;
flex-shrink: 0;
}
.brand-text { }
.brand-text h1 {
font-size: 22px;
font-weight: 700;
color: var(--text);
letter-spacing: -0.02em;
line-height: 1.2;
}
.brand-text .subtitle {
font-size: 13px;
color: var(--text-muted);
margin-top: 2px;
}
.report-meta {
text-align: right;
flex-shrink: 0;
}
.report-meta .meta-line {
font-size: 12px;
color: var(--text-muted);
font-family: var(--mono);
line-height: 1.8;
}
.report-meta .meta-line strong {
color: var(--text);
font-weight: 600;
}
/* SUMMARY CARDS */
.cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 28px;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 20px 22px;
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 3px;
background: var(--accent);
}
.card.warn::before { background: #e09a3a; }
.card.crit::before { background: #e05252; }
.card.good::before { background: #2e9e66; }
.card-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
margin-bottom: 6px;
}
.card-value {
font-size: 28px;
font-weight: 700;
font-family: var(--mono);
color: var(--text);
letter-spacing: -0.02em;
line-height: 1;
}
.card-sub {
font-size: 12px;
color: var(--text-muted);
margin-top: 4px;
}
.card-badge {
display: inline-block;
margin-top: 8px;
padding: 2px 10px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.badge-crit { background: var(--red-lt); color: var(--red); }
.badge-warn { background: var(--amber-lt); color: var(--amber); }
.badge-good { background: var(--green-lt); color: var(--green); }
/* DRIVE BAR */
.drive-bar-wrap {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 22px 26px;
margin-bottom: 28px;
}
.drive-bar-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 10px;
}
.drive-bar-title {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--text-muted);
}
.drive-bar-legend {
display: flex;
gap: 20px;
font-size: 12px;
color: var(--text-muted);
font-family: var(--mono);
}
.drive-bar-legend span strong { color: var(--text); }
.bar-track {
width: 100%;
height: 18px;
background: #e8eaf0;
border-radius: 9px;
overflow: hidden;
position: relative;
}
.bar-used {
height: 100%;
border-radius: 9px;
transition: width 0.4s ease;
position: relative;
}
.bar-recoverable-marker {
position: absolute;
top: 0;
height: 100%;
background: repeating-linear-gradient(
135deg,
rgba(255,255,255,0.25) 0px,
rgba(255,255,255,0.25) 4px,
transparent 4px,
transparent 8px
);
border-right: 2px dashed rgba(255,255,255,0.6);
}
.bar-labels {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 11px;
color: var(--text-muted);
font-family: var(--mono);
}
.bar-label-recoverable {
color: #e09a3a;
font-weight: 600;
}
/* SECTION TITLE */
.section-title {
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.09em;
color: var(--text-muted);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 10px;
}
.section-title::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
/* TABLE */
.findings-table-wrap {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
margin-bottom: 28px;
}
table {
width: 100%;
border-collapse: collapse;
}
thead th {
background: #1a2033;
color: #a8b4cc;
font-size: 10.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
padding: 12px 16px;
text-align: left;
white-space: nowrap;
}
tbody tr {
border-bottom: 1px solid var(--border);
transition: background 0.12s;
}
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: #f8f9fc; }
td {
padding: 12px 16px;
vertical-align: top;
font-size: 13px;
}
.cat-cell {
font-size: 12px;
font-weight: 600;
color: var(--accent);
white-space: nowrap;
vertical-align: top;
border-right: 2px solid var(--accent-lt);
background: var(--accent-lt);
width: 160px;
}
.cat-total {
display: block;
font-size: 10px;
font-weight: 700;
color: var(--text-muted);
font-family: var(--mono);
margin-top: 3px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.path {
font-family: var(--mono);
font-size: 10.5px;
color: var(--text-muted);
display: block;
margin-top: 3px;
}
.desc-cell {
color: var(--text-muted);
font-size: 12.5px;
max-width: 340px;
}
.note {
display: inline-block;
margin-top: 4px;
font-size: 11px;
font-weight: 600;
color: var(--amber);
background: var(--amber-lt);
padding: 1px 7px;
border-radius: 4px;
}
.size-cell {
font-family: var(--mono);
font-size: 13px;
font-weight: 600;
white-space: nowrap;
text-align: right;
}
.sev-badge-cell { width: 80px; text-align: center; }
.badge {
display: inline-block;
padding: 3px 10px;
border-radius: 20px;
font-size: 10.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
white-space: nowrap;
}
.sev-critical { color: var(--red); }
.sev-warning { color: var(--amber); }
.sev-minor { color: var(--blue); }
.sev-ok { color: #6b7591; }
.badge.sev-critical { background: var(--red-lt); color: var(--red); }
.badge.sev-warning { background: var(--amber-lt); color: var(--amber); }
.badge.sev-minor { background: var(--blue-lt); color: var(--blue); }
.badge.sev-ok { background: #f0f1f4; color: #6b7591; }
/* LEGEND */
.legend-row {
display: flex;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 28px;
font-size: 12px;
color: var(--text-muted);
align-items: center;
}
.legend-item { display: flex; align-items: center; gap: 7px; }
/* RECOMMENDATION BOX */
.rec-box {
background: var(--surface);
border: 1px solid var(--border);
border-left: 4px solid var(--accent);
border-radius: 10px;
padding: 20px 24px;
margin-bottom: 28px;
}
.rec-box h3 {
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--accent);
margin-bottom: 12px;
}
.rec-list { list-style: none; }
.rec-list li {
padding: 6px 0;
border-bottom: 1px solid var(--border);
font-size: 13px;
display: flex;
align-items: baseline;
gap: 10px;
}
.rec-list li:last-child { border-bottom: none; }
.rec-icon {
flex-shrink: 0;
font-size: 13px;
width: 20px;
text-align: center;
}
/* FOOTER */
.report-footer {
margin-top: 44px;
padding-top: 20px;
border-top: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
color: var(--text-muted);
font-family: var(--mono);
}
/* PRINT */
@media print {
body { background: #fff; font-size: 12px; }
.page-wrap { padding: 20px; max-width: 100%; }
.card-value { font-size: 22px; }
tbody tr:hover { background: none; }
.findings-table-wrap { box-shadow: none; border: 1px solid #ccc; }
}
</style>
</head>
<body>
<div class="page-wrap">
<!-- HEADER -->
<div class="report-header">
<div class="brand">
<div class="brand-logo">SBC<br>IT</div>
<div class="brand-text">
<h1>Disk Space Report</h1>
<div class="subtitle">$reportTitle &mdash; C: Drive Analysis</div>
</div>
</div>
<div class="report-meta">
<div class="meta-line"><strong>Host</strong> &nbsp;$($env:COMPUTERNAME)</div>
<div class="meta-line"><strong>OS</strong> &nbsp;$osName $osBuild</div>
<div class="meta-line"><strong>Uptime</strong> &nbsp;$uptime</div>
<div class="meta-line"><strong>Generated</strong> &nbsp;$timestamp</div>
<div class="meta-line"><strong>Prepared by</strong> &nbsp;SBCIT</div>
</div>
</div>
<!-- SUMMARY CARDS -->
<div class="cards">
<div class="card $(if ($driveStatus -eq 'Critical') {'crit'} elseif ($driveStatus -eq 'Low') {'warn'} else {'good'})">
<div class="card-label">Drive Status</div>
<div class="card-value">$driveStatus</div>
<div class="card-sub">$freePct% free of $totalGB GB total</div>
<span class="card-badge $(if ($driveStatus -eq 'Critical') {'badge-crit'} elseif ($driveStatus -eq 'Low') {'badge-warn'} else {'badge-good'})">$driveStatus</span>
</div>
<div class="card">
<div class="card-label">Currently Free</div>
<div class="card-value">$freeGB<span style="font-size:14px;font-weight:400"> GB</span></div>
<div class="card-sub">$freePct% of total capacity</div>
</div>
<div class="card warn">
<div class="card-label">Recoverable Space</div>
<div class="card-value">$(Format-Size $totalRecoverable)</div>
<div class="card-sub">$recoverablePct% of total capacity</div>
</div>
<div class="card good">
<div class="card-label">Projected Free (After)</div>
<div class="card-value">$projectedFreeGB<span style="font-size:14px;font-weight:400"> GB</span></div>
<div class="card-sub">$projectedFreePct% if all items cleaned</div>
</div>
</div>
<!-- DRIVE BAR -->
<div class="drive-bar-wrap">
<div class="drive-bar-header">
<div class="drive-bar-title">C: Drive Capacity</div>
<div class="drive-bar-legend">
<span><strong>$usedGB GB</strong> used</span>
<span><strong>$(Format-Size $totalRecoverable)</strong> recoverable</span>
<span><strong>$freeGB GB</strong> free</span>
<span><strong>$totalGB GB</strong> total</span>
</div>
</div>
<div class="bar-track">
<div class="bar-used" style="width:${barUsed}%; background:$barUsedColor;">
<div class="bar-recoverable-marker" style="right:0; width:$(if ($barUsed -gt 0) { [math]::Round(($totalRecoverable / $driveTotal) * 100, 1) } else { 0 })%;"></div>
</div>
</div>
<div class="bar-labels">
<span>0 GB</span>
<span class="bar-label-recoverable">&#x21A5; $(Format-Size $totalRecoverable) recoverable</span>
<span>$totalGB GB</span>
</div>
</div>
<!-- LEGEND -->
<div class="legend-row">
<span style="font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;">Impact:</span>
<div class="legend-item"><span class="badge sev-critical">High</span> <span>&ge; 1 GB</span></div>
<div class="legend-item"><span class="badge sev-warning">Medium</span> <span>200 MB &ndash; 1 GB</span></div>
<div class="legend-item"><span class="badge sev-minor">Low</span> <span>10 MB &ndash; 200 MB</span></div>
<div class="legend-item"><span class="badge sev-ok">Minimal</span> <span>&lt; 10 MB</span></div>
</div>
<!-- FINDINGS TABLE -->
<div class="section-title">Findings All Scanned Categories</div>
<div class="findings-table-wrap">
<table>
<thead>
<tr>
<th>Category</th>
<th>Item</th>
<th>Description</th>
<th style="text-align:right">Size</th>
<th style="text-align:center">Impact</th>
</tr>
</thead>
<tbody>
$($rowsHtml.ToString())
</tbody>
</table>
</div>
<!-- RECOMMENDATION BOX -->
<div class="section-title">Recommendations</div>
<div class="rec-box">
<h3>Suggested Actions</h3>
<ul class="rec-list">
$(
$recLines = [System.Text.StringBuilder]::new()
if ($totalRecoverable -gt 5GB) {
[void]$recLines.AppendLine(" <li><span class='rec-icon'>&#x26A0;</span> Immediate cleanup is recommended. Total recoverable space exceeds 5 GB, which may impact server performance and reliability.</li>")
} elseif ($totalRecoverable -gt 1GB) {
[void]$recLines.AppendLine(" <li><span class='rec-icon'>&#x2139;</span> Cleanup is advisable at the next available maintenance window. Over 1 GB of recoverable space was found.</li>")
} else {
[void]$recLines.AppendLine(" <li><span class='rec-icon'>&#x2714;</span> No urgent action required. Recoverable space is within acceptable limits.</li>")
}
if ($freePct -lt 15) {
[void]$recLines.AppendLine(" <li><span class='rec-icon'>&#x26A0;</span> <strong>Free space is critically low ($freePct%).</strong> Disk pressure at this level can cause service failures, failed backups, and event log interruption. Escalate promptly.</li>")
}
if ($evtxSize -gt 500MB) {
[void]$recLines.AppendLine(" <li><span class='rec-icon'>&#x1F4CB;</span> Event logs are consuming $(Format-Size $evtxSize). These will be automatically backed up to secure storage before clearing.</li>")
}
if ($memDump -gt 0) {
[void]$recLines.AppendLine(" <li><span class='rec-icon'>&#x26A0;</span> A full memory dump (MEMORY.DMP) was found ($(Format-Size $memDump)). This indicates a past system crash. Recommend reviewing crash cause before removing.</li>")
}
$wuSize = ($findings | Where-Object { $_.Category -eq 'Windows Update' } | Measure-Object -Property Size -Sum).Sum
if ($wuSize -gt 1GB) {
[void]$recLines.AppendLine(" <li><span class='rec-icon'>&#x1F504;</span> Windows Update cache is $(Format-Size $wuSize). Safe to clear after confirming no updates are pending installation.</li>")
}
$iisSize = ($findings | Where-Object { $_.Category -eq 'IIS Web Server Logs' } | Measure-Object -Property Size -Sum).Sum
if ($iisSize -gt 0) {
[void]$recLines.AppendLine(" <li><span class='rec-icon'>&#x1F4C4;</span> IIS logs ($( Format-Size $iisSize)) can be reduced by removing files older than 30 days. Consider implementing automated log rotation.</li>")
}
[void]$recLines.AppendLine(" <li><span class='rec-icon'>&#x1F527;</span> Run cleanup via the SBCIT disk cleanup script (<code>Invoke-DiskCleanupScan.ps1</code>) which handles all items above safely with confirmation prompts.</li>")
$recLines.ToString()
)
</ul>
</div>
<!-- FOOTER -->
<div class="report-footer">
<span>SBCIT Managed Services &mdash; Confidential</span>
<span>$($env:COMPUTERNAME) &mdash; $timestamp</span>
</div>
</div>
</body>
</html>
"@
# ─────────────────────────────────────────────
# WRITE FILE
# ─────────────────────────────────────────────
# Need HttpUtility for HTML encoding — load System.Web
Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue
$html | Out-File -FilePath $reportFile -Encoding UTF8
Write-Host ""
Write-Host " ✔ Report generated:" -ForegroundColor Green
Write-Host " $reportFile" -ForegroundColor Cyan
Write-Host ""
Write-Host (" Total Recoverable : {0}" -f (Format-Size $totalRecoverable)) -ForegroundColor Yellow
Write-Host (" Current Free : $freeGB GB ($freePct%)") -ForegroundColor Gray
Write-Host (" Projected Free : $projectedFreeGB GB ($projectedFreePct%)") -ForegroundColor Gray
Write-Host ""
# Open in default browser
try {
Start-Process $reportFile
} catch {}