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"
This commit is contained in:
837
Powershell/Invoke-DiskSpaceReport.ps1
Normal file
837
Powershell/Invoke-DiskSpaceReport.ps1
Normal file
@@ -0,0 +1,837 @@
|
|||||||
|
#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 — C: Drive Analysis</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="report-meta">
|
||||||
|
<div class="meta-line"><strong>Host</strong> $($env:COMPUTERNAME)</div>
|
||||||
|
<div class="meta-line"><strong>OS</strong> $osName $osBuild</div>
|
||||||
|
<div class="meta-line"><strong>Uptime</strong> $uptime</div>
|
||||||
|
<div class="meta-line"><strong>Generated</strong> $timestamp</div>
|
||||||
|
<div class="meta-line"><strong>Prepared by</strong> 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">↥ $(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>≥ 1 GB</span></div>
|
||||||
|
<div class="legend-item"><span class="badge sev-warning">Medium</span> <span>200 MB – 1 GB</span></div>
|
||||||
|
<div class="legend-item"><span class="badge sev-minor">Low</span> <span>10 MB – 200 MB</span></div>
|
||||||
|
<div class="legend-item"><span class="badge sev-ok">Minimal</span> <span>< 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'>⚠</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'>ℹ</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'>✔</span> No urgent action required. Recoverable space is within acceptable limits.</li>")
|
||||||
|
}
|
||||||
|
if ($freePct -lt 15) {
|
||||||
|
[void]$recLines.AppendLine(" <li><span class='rec-icon'>⚠</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'>📋</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'>⚠</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'>🔄</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'>📄</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'>🔧</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 — Confidential</span>
|
||||||
|
<span>$($env:COMPUTERNAME) — $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 {}
|
||||||
Reference in New Issue
Block a user