P05: System Health Dashboard Generator

P05: System Health Dashboard Generator

Project Overview

What you’ll build: A PowerShell script that collects system metrics (CPU, memory, disk, running services, recent errors) and generates an HTML report you can open in a browser.

Attribute Value
Difficulty Level 1: Beginner
Time Estimate Weekend (8-12 hours)
Programming Language PowerShell
Knowledge Area System Administration
Prerequisites Basic command-line familiarity

1. Learning Objectives

After completing this project, you will be able to:

  1. Query system information with WMI/CIM - Access CPU, memory, disk data programmatically, treating Windows as a queryable database
  2. Master the PowerShell pipeline paradigm - Understand that PowerShell pipes objects, not text, fundamentally changing how you think about data transformation
  3. Filter and transform data - Use Where-Object and Select-Object effectively for projection and filtering
  4. Create calculated properties - Transform raw values (bytes) into human-readable formats (GB) on the fly
  5. Generate formatted HTML output - Create professional reports from PowerShell objects
  6. Handle errors gracefully - Use try/catch for robust, production-ready scripts
  7. Create reusable parameters - Build scripts with command-line arguments using the param() block
  8. Implement status thresholds - Design logic that classifies metrics as normal, warning, or critical

2. Deep Theoretical Foundation

WMI and CIM: Windows as a Queryable Database

What is WMI?

Windows Management Instrumentation (WMI) is Windows’ implementation of WBEM (Web-Based Enterprise Management). Think of it as a massive database that contains everything about your system - hardware, software, configuration, running processes, and more.

The key insight: WMI treats your entire Windows system as queryable data. Instead of parsing text output from commands, you query structured objects with properties and methods.

The WMI Repository Architecture:

+------------------------------------------------------------------+
|                        WMI Architecture                           |
+------------------------------------------------------------------+
|                                                                   |
|    +-----------------+     +------------------+                   |
|    |   Your Script   |     |   Other Tools    |                   |
|    |  (PowerShell)   |     | (WMI Explorer)   |                   |
|    +--------+--------+     +--------+---------+                   |
|             |                       |                             |
|             v                       v                             |
|    +------------------------------------------+                   |
|    |            WMI Service (winmgmt)         |                   |
|    |  - Receives queries                       |                   |
|    |  - Routes to appropriate provider         |                   |
|    |  - Returns structured objects             |                   |
|    +------------------+----------------------+                    |
|                       |                                           |
|    +------------------v----------------------+                    |
|    |            WMI Repository               |                    |
|    |                                          |                    |
|    |  Namespace: root\cimv2 (most common)    |                    |
|    |  +----------------------------------+   |                    |
|    |  | Win32_OperatingSystem            |   |                    |
|    |  |  - Caption: "Windows 11 Pro"     |   |                    |
|    |  |  - TotalVisibleMemorySize        |   |                    |
|    |  |  - FreePhysicalMemory            |   |                    |
|    |  |  - LastBootUpTime                |   |                    |
|    |  +----------------------------------+   |                    |
|    |  | Win32_Processor                  |   |                    |
|    |  |  - Name: "Intel Core i7-12700"   |   |                    |
|    |  |  - NumberOfCores: 12             |   |                    |
|    |  |  - LoadPercentage: 25            |   |                    |
|    |  +----------------------------------+   |                    |
|    |  | Win32_LogicalDisk                |   |                    |
|    |  |  - DeviceID: "C:"                |   |                    |
|    |  |  - Size: 512110190592            |   |                    |
|    |  |  - FreeSpace: 256055095296       |   |                    |
|    |  +----------------------------------+   |                    |
|    |  | Win32_Service                    |   |                    |
|    |  |  - Name: "wuauserv"              |   |                    |
|    |  |  - State: "Running"              |   |                    |
|    |  +----------------------------------+   |                    |
|    +------------------------------------------+                   |
|                       |                                           |
|    +------------------v----------------------+                    |
|    |            WMI Providers               |                     |
|    |  - Query actual hardware/software       |                     |
|    |  - Return real-time data                |                     |
|    +------------------------------------------+                   |
+------------------------------------------------------------------+

WMI vs CIM - The Evolution:

Aspect WMI (Legacy) CIM (Modern)
Cmdlets Get-WmiObject Get-CimInstance
Protocol DCOM (Distributed COM) WS-Man (WinRM)
Performance Slower, heavier Faster, more efficient
Remote support Requires DCOM configuration Uses standard WinRM
Firewall friendly No (dynamic ports) Yes (single port 5985/5986)
PowerShell version All versions 3.0+
Future Deprecated Actively developed

Best Practice: Always use CIM (Get-CimInstance) for new scripts. WMI is legacy.

Key CIM Classes for System Health:

Class Information Provided Example Query
Win32_OperatingSystem OS version, memory, uptime, boot time Get-CimInstance Win32_OperatingSystem
Win32_Processor CPU name, cores, current load Get-CimInstance Win32_Processor
Win32_LogicalDisk Drive letters, sizes, free space Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3"
Win32_Service Windows services and their states Get-CimInstance Win32_Service
Win32_Process Running processes Get-CimInstance Win32_Process
Win32_PhysicalMemory RAM modules, speeds Get-CimInstance Win32_PhysicalMemory
Win32_ComputerSystem Computer name, domain, manufacturer Get-CimInstance Win32_ComputerSystem
Win32_NetworkAdapterConfiguration IP addresses, DNS settings Get-CimInstance Win32_NetworkAdapterConfiguration

Why CIM is Better for Remote Administration:

Traditional WMI (DCOM):
+----------+                           +----------+
| Client   |----[Dynamic Ports]------->| Server   |
|          |<---[DCOM Callbacks]-------|          |
+----------+  (Firewall nightmare!)    +----------+

Modern CIM (WS-Man):
+----------+                           +----------+
| Client   |----[Port 5985/5986]------>| Server   |
|          |<---[Same Port]------------|          |
+----------+  (Single port, firewall   +----------+
               friendly!)

The Pipeline Paradigm: Objects, Not Text

This is the single most important concept that separates PowerShell from traditional shells like Bash. Understanding this deeply will transform how you write scripts.

The Unix/Bash Paradigm (Text Streams):

In Bash, everything is text. Commands emit lines of text, and you parse that text:

# Get process ID of firefox
ps aux | grep firefox | awk '{print $2}'

# What's happening:
# 1. ps aux outputs TEXT (columns separated by spaces)
# 2. grep filters lines containing "firefox" (pattern matching on TEXT)
# 3. awk extracts the 2nd column (splitting TEXT on whitespace)

# PROBLEMS:
# - Column positions can vary
# - You're constantly parsing, splitting, and reassembling strings
# - No type safety - everything is a string
# - Fragile: if output format changes, script breaks

The PowerShell Paradigm (Object Streams):

In PowerShell, everything is an object with properties and methods:

# Get process ID of firefox
Get-Process firefox | Select-Object Id

# What's happening:
# 1. Get-Process returns OBJECTS with properties (Name, Id, CPU, etc.)
# 2. Select-Object picks specific properties from those objects
# 3. No text parsing needed!

# ADVANTAGES:
# - Properties are named, not positional
# - Type safety - integers are integers, dates are dates
# - Discoverable - use Get-Member to explore any object
# - Robust - property names don't change like column positions

Visualizing the Pipeline Data Flow:

Text-Based Pipeline (Bash):
+-------------+    "firefox 1234 0.5 ..."    +--------+    "firefox"    +-------+
| ps aux      |----------------------------->| grep   |---------------->| awk   |
+-------------+   (Line of text)             +--------+  (Text match)   +-------+
                                                                            |
                                                                      "1234" (string)

Object-Based Pipeline (PowerShell):
+-------------+    ProcessObject              +--------------+    ProcessObject
| Get-Process |--[{Name:"firefox",Id:1234,-->| Where-Object |--[{Name:"firefox",
+-------------+    CPU:0.5, ...}]             +--------------+    Id:1234,...}]
                                                                        |
                                                                        v
                                                                  +-------------+
                                                                  | Select-Object
                                                                  +-------------+
                                                                        |
                                                                  1234 (Int32)

The Pipeline in Detail:

+------------------------------------------------------------------------+
|                    PowerShell Pipeline Architecture                      |
+------------------------------------------------------------------------+
|                                                                          |
|  Get-CimInstance Win32_LogicalDisk                                      |
|        |                                                                 |
|        | Returns array of objects:                                       |
|        |                                                                 |
|        v                                                                 |
|  +------------------------------------------------------------------+   |
|  | Object 1:                                                         |   |
|  |   DeviceID    : C:                                               |   |
|  |   Size        : 512110190592                                     |   |
|  |   FreeSpace   : 256055095296                                     |   |
|  |   DriveType   : 3                                                |   |
|  |   VolumeName  : System                                           |   |
|  +------------------------------------------------------------------+   |
|  +------------------------------------------------------------------+   |
|  | Object 2:                                                         |   |
|  |   DeviceID    : D:                                               |   |
|  |   Size        : 1099511627776                                    |   |
|  |   FreeSpace   : 879609302221                                     |   |
|  |   DriveType   : 3                                                |   |
|  |   VolumeName  : Data                                             |   |
|  +------------------------------------------------------------------+   |
|        |                                                                 |
|        v                                                                 |
|  Where-Object { $_.FreeSpace -lt 300GB }                                |
|        |                                                                 |
|        | Filters objects where FreeSpace < 300GB:                        |
|        | (D: has 820GB free, so it's removed)                           |
|        |                                                                 |
|        v                                                                 |
|  +------------------------------------------------------------------+   |
|  | Object 1:                                                         |   |
|  |   DeviceID    : C:                                               |   |
|  |   Size        : 512110190592                                     |   |
|  |   FreeSpace   : 256055095296  (238GB, less than 300GB)          |   |
|  +------------------------------------------------------------------+   |
|        |                                                                 |
|        v                                                                 |
|  Select-Object DeviceID, @{N='FreeGB'; E={[math]::Round($_.FreeSpace/1GB)}}
|        |                                                                 |
|        | Projects specific properties (with transformation):             |
|        |                                                                 |
|        v                                                                 |
|  +------------------------------------------------------------------+   |
|  | Result Object:                                                    |   |
|  |   DeviceID : C:                                                  |   |
|  |   FreeGB   : 238                                                 |   |
|  +------------------------------------------------------------------+   |
|                                                                          |
+------------------------------------------------------------------------+

Key Pipeline Cmdlets:

Cmdlet Purpose Example
Where-Object Filter objects by condition Get-Process \| Where-Object CPU -gt 10
Select-Object Choose/rename/calculate properties Get-Process \| Select-Object Name, Id
Sort-Object Order by property Get-Process \| Sort-Object CPU -Descending
Group-Object Group by property value Get-Service \| Group-Object Status
Measure-Object Calculate statistics Get-Process \| Measure-Object CPU -Sum -Average
ForEach-Object Transform each object 1..10 \| ForEach-Object { $_ * 2 }

Object Filtering and Projection

Where-Object: The Filter

Where-Object (alias: ? or where) filters objects based on a condition. Only objects that pass the test continue down the pipeline.

# Full syntax
Get-Service | Where-Object { $_.Status -eq 'Stopped' }

# Simplified syntax (PowerShell 3+)
Get-Service | Where-Object Status -eq 'Stopped'

# Using alias
Get-Service | ? Status -eq 'Stopped'

The Automatic Variable $_:

Inside a pipeline script block { }, the special variable $_ represents “the current object”:

Get-Process | Where-Object { $_.CPU -gt 100 -and $_.Name -like "*chrome*" }
#                             ^^^                  ^^^
#                    Current process         Current process

Select-Object: The Projector

Select-Object (alias: select) chooses which properties to keep, can rename them, or calculate new ones.

# Simple selection
Get-Process | Select-Object Name, Id, CPU

# First/Last N items
Get-Process | Sort-Object CPU -Descending | Select-Object -First 5

# Unique values
Get-Process | Select-Object Name -Unique

Calculated Properties - The Power Feature:

Calculated properties let you transform data on the fly:

# Syntax
@{
    Name = 'NewPropertyName'      # or use 'N', 'Label', or 'L'
    Expression = { $_.xxx }        # or use 'E'
}

# Example: Convert bytes to GB
Get-CimInstance Win32_LogicalDisk | Select-Object DeviceID,
    @{Name='SizeGB'; Expression={[math]::Round($_.Size/1GB, 2)}},
    @{Name='FreeGB'; Expression={[math]::Round($_.FreeSpace/1GB, 2)}},
    @{Name='UsedPercent'; Expression={
        [math]::Round(($_.Size - $_.FreeSpace) / $_.Size * 100, 1)
    }}

# Output:
# DeviceID  SizeGB  FreeGB  UsedPercent
# --------  ------  ------  -----------
# C:        476.94  238.47  50.0
# D:        1024.00 819.32  20.0

HTML Generation and Formatting

PowerShell can convert any object directly to HTML, making report generation trivial.

Basic HTML Conversion:

# Simple conversion - produces a complete HTML page
Get-Process | ConvertTo-Html | Out-File processes.html

# With title and basic styling
Get-Process | ConvertTo-Html -Title "Process Report" | Out-File report.html

The -Fragment Parameter:

When building multi-section reports, use -Fragment to get just the table:

# Full HTML document
Get-Process | ConvertTo-Html                    # Includes <html>, <head>, <body>

# Just the table
Get-Process | ConvertTo-Html -Fragment          # Only <table>...</table>

Building Professional Reports:

+------------------------------------------------------------------------+
|                    HTML Report Generation Flow                          |
+------------------------------------------------------------------------+
|                                                                          |
|  $cpuData = Get-SystemMetrics                                           |
|       |                                                                  |
|       v                                                                  |
|  $cpuData | ConvertTo-Html -Fragment                                    |
|       |                                                                  |
|       v                                                                  |
|  "<table><tr><th>CPU</th>...</table>"                                   |
|                                                                          |
|  $diskData = Get-DiskMetrics                                            |
|       |                                                                  |
|       v                                                                  |
|  $diskData | ConvertTo-Html -Fragment                                   |
|       |                                                                  |
|       v                                                                  |
|  "<table><tr><th>Drive</th>...</table>"                                 |
|                                                                          |
|                        +                                                 |
|                        |                                                 |
|  $css = "<style>table{...}th{...}</style>"                              |
|                        |                                                 |
|                        v                                                 |
|  +------------------------------------------------------------------+   |
|  | $html = @"                                                        |   |
|  | <!DOCTYPE html>                                                   |   |
|  | <html><head>$css</head>                                          |   |
|  | <body>                                                            |   |
|  |   <h1>System Health Report</h1>                                   |   |
|  |   <h2>CPU & Memory</h2>                                           |   |
|  |   $cpuTable                                                       |   |
|  |   <h2>Disk Usage</h2>                                             |   |
|  |   $diskTable                                                      |   |
|  | </body></html>                                                    |   |
|  | "@                                                                |   |
|  +------------------------------------------------------------------+   |
|                        |                                                 |
|                        v                                                 |
|  $html | Out-File report.html                                           |
|                                                                          |
+------------------------------------------------------------------------+

Adding Conditional Formatting with String Replacement:

# Convert data to HTML
$html = $diskData | ConvertTo-Html -Fragment

# Replace status values with colored spans
$html = $html -replace '<td>CRITICAL</td>', '<td class="critical">CRITICAL</td>'
$html = $html -replace '<td>WARNING</td>', '<td class="warning">WARNING</td>'
$html = $html -replace '<td>NORMAL</td>', '<td class="normal">NORMAL</td>'

Error Handling in Scripts

Robust scripts anticipate and handle errors gracefully.

Try/Catch/Finally:

try {
    # Code that might fail
    $data = Get-CimInstance Win32_Service -ErrorAction Stop
}
catch {
    # Handle the error
    Write-Warning "Failed to get services: $_"
    $data = @()  # Provide fallback
}
finally {
    # Always runs (cleanup)
    Write-Verbose "Query completed"
}

ErrorAction Parameter:

Controls how cmdlets respond to non-terminating errors:

Value Behavior
Continue Display error, continue (default)
SilentlyContinue Suppress error, continue
Stop Convert to terminating error (catchable)
Ignore Suppress completely (even $Error)

Best Practice Pattern:

function Get-SafeData {
    param([string]$Query)

    try {
        Get-CimInstance $Query -ErrorAction Stop
    }
    catch [Microsoft.Management.Infrastructure.CimException] {
        # Specific WMI/CIM error
        Write-Warning "CIM query failed: $($_.Exception.Message)"
        return $null
    }
    catch {
        # Any other error
        Write-Error "Unexpected error: $_"
        throw
    }
}

3. Complete Project Specification

Functional Requirements

ID Requirement Priority Notes
F1 Collect CPU usage percentage Must Have Use Win32_Processor.LoadPercentage
F2 Collect memory usage (total, used, free, percent) Must Have Calculate from Win32_OperatingSystem
F3 Collect disk space for all local drives Must Have Filter DriveType=3 (local fixed)
F4 Show system uptime Must Have Calculate from LastBootUpTime
F5 Generate HTML report with styling Must Have CSS for professional appearance
F6 Color-code status (normal/warning/critical) Must Have Visual indicator of health
F7 Show critical Windows services status Should Have Configurable service list
F8 Show recent error events (last 24h) Should Have From Application/System logs
F9 Support -OutputPath parameter Should Have Custom save location
F10 Support -OpenInBrowser switch Should Have Auto-open after generation
F11 Support -EmailTo parameter Nice to Have Send report via email
F12 Export to PDF Nice to Have Requires external tooling

Non-Functional Requirements

ID Requirement Rationale
NF1 Script completes in < 10 seconds User experience - no waiting
NF2 Works without administrator privileges Most data available to standard users
NF3 Compatible with PowerShell 5.1+ and 7+ Support both Windows PowerShell and PowerShell Core
NF4 Gracefully handles missing data Don’t crash if a query fails
NF5 UTF-8 output encoding Proper character handling
NF6 No external dependencies Single-file script

Thresholds Specification

$Thresholds = @{
    CPU = @{
        Warning = 80    # 80% or higher = warning
        Critical = 95   # 95% or higher = critical
    }
    Memory = @{
        Warning = 75    # 75% used = warning
        Critical = 90   # 90% used = critical
    }
    Disk = @{
        Warning = 80    # 80% used = warning
        Critical = 95   # 95% used = critical
    }
    Uptime = @{
        Warning = 90    # 90 days without reboot = warning
        Critical = 180  # 180 days = critical (needs patching!)
    }
}

4. Real World Outcome

Example Command Execution

Basic Usage:

PS C:\Scripts> .\Get-SystemHealth.ps1
Collecting system metrics...
  [OK] CPU and Memory
  [OK] Disk Information
  [OK] Service Status
  [OK] Recent Errors (3 found)
Generating HTML report...
Report saved to: C:\Users\Admin\AppData\Local\Temp\SystemHealth_20251226_142345.html
Opening in browser...

With Parameters:

# Save to specific location
PS> .\Get-SystemHealth.ps1 -OutputPath "C:\Reports\health.html"

# Don't auto-open browser
PS> .\Get-SystemHealth.ps1 -OutputPath "C:\Reports\health.html" -OpenInBrowser:$false

# Email the report (when implemented)
PS> .\Get-SystemHealth.ps1 -EmailTo "admin@company.com"

Example HTML Report Output (ASCII Mockup)

+=========================================================================+
|                      SYSTEM HEALTH REPORT                                |
|   Computer: WORKSTATION-01  |  Generated: 2025-12-26 14:23:45           |
+=========================================================================+

  +------------+    +------------+    +------------+
  |    47%     |    |   65.2%    |    |  45 days   |
  | CPU Usage  |    |Memory Used |    |   Uptime   |
  |  [NORMAL]  |    |  [NORMAL]  |    |  [NORMAL]  |
  +------------+    +------------+    +------------+

+-------------------------------------------------------------------------+
| SYSTEM INFORMATION                                                       |
+-------------------------------------------------------------------------+
| OS Version:    Microsoft Windows 11 Pro                                  |
| CPU:           AMD Ryzen 9 5900X 12-Core Processor (12 cores)           |
| Total Memory:  32.00 GB                                                  |
| Last Boot:     2025-11-11 09:15:22                                      |
+-------------------------------------------------------------------------+

+-------------------------------------------------------------------------+
| DISK USAGE                                                               |
+-------------------------------------------------------------------------+
| Drive | Label    | Size (GB) | Used (GB) | Free (GB) | Used % | Status  |
|-------|----------|-----------|-----------|-----------|--------|---------|
| C:    | System   |   476.94  |   238.47  |   238.47  |  50.0% | NORMAL  |
| D:    | Data     |  1024.00  |   921.60  |   102.40  |  90.0% |CRITICAL |
| E:    | Backup   |   512.00  |   409.60  |   102.40  |  80.0% | WARNING |
+-------------------------------------------------------------------------+
       (Color coded: NORMAL=Green, WARNING=Yellow, CRITICAL=Red)

+-------------------------------------------------------------------------+
| CRITICAL SERVICES                                                        |
+-------------------------------------------------------------------------+
| Service Name              | Status      | Health      |
|---------------------------|-------------|-------------|
| Windows Update            | Running     | NORMAL      |
| Windows Defender          | Running     | NORMAL      |
| Windows Event Log         | Running     | NORMAL      |
| Print Spooler             | Stopped     | WARNING     |
| SQL Server (MSSQLSERVER)  | Not Installed| INFO       |
+-------------------------------------------------------------------------+

+-------------------------------------------------------------------------+
| RECENT ERRORS (Last 24 Hours)                                            |
+-------------------------------------------------------------------------+
| Time                | Level    | Source          | Message              |
|---------------------|----------|-----------------|----------------------|
| 2025-12-26 13:45:22 | Error    | Application     | App crashed unexpe...|
| 2025-12-26 12:10:19 | Error    | WindowsUpdate   | Update KB5034441 fa...|
| 2025-12-26 09:30:05 | Critical | Disk            | Drive D: running low.|
+-------------------------------------------------------------------------+

                    Report generated by Get-SystemHealth.ps1
                              PowerShell 7.4.1
+=========================================================================+

Color-Coded Status Indicators

The HTML report uses CSS classes for visual distinction:

Status CSS Class Background Text Color When Applied
NORMAL .normal Light Green (#d4edda) Dark Green (#155724) Metric within healthy range
WARNING .warning Light Yellow (#fff3cd) Dark Yellow (#856404) Approaching threshold
CRITICAL .critical Light Red (#f8d7da) Dark Red (#721c24) Exceeded threshold
INFO .info Light Blue (#cce5ff) Dark Blue (#004085) Informational (not applicable)

Scheduling Options

Windows Task Scheduler (Recommended):

# Create scheduled task to run daily at 8 AM
$action = New-ScheduledTaskAction -Execute 'PowerShell.exe' `
    -Argument '-NoProfile -File "C:\Scripts\Get-SystemHealth.ps1" -OutputPath "C:\Reports\daily_$(Get-Date -Format yyyyMMdd).html"'

$trigger = New-ScheduledTaskTrigger -Daily -At '8:00 AM'

$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount

Register-ScheduledTask -TaskName 'DailyHealthReport' `
    -Action $action -Trigger $trigger -Principal $principal

Continuous Monitoring (Run Every Hour):

# Using Task Scheduler repeat interval
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) `
    -RepetitionInterval (New-TimeSpan -Hours 1) `
    -RepetitionDuration (New-TimeSpan -Days 365)

5. Solution Architecture

Component Diagram

+=========================================================================+
|                 SYSTEM HEALTH DASHBOARD GENERATOR                        |
+=========================================================================+
|                                                                          |
|  +---------------------------+    +---------------------------+          |
|  |     PARAMETER HANDLER     |    |    THRESHOLD CONFIG       |          |
|  |---------------------------|    |---------------------------|          |
|  | - Parse command line      |    | - CPU thresholds          |          |
|  | - Set defaults            |    | - Memory thresholds       |          |
|  | - Validate paths          |    | - Disk thresholds         |          |
|  +------------+--------------+    | - Uptime thresholds       |          |
|               |                   +------------+--------------+          |
|               v                                |                         |
|  +---------------------------------------------v-----------------------+ |
|  |                        DATA COLLECTORS                              | |
|  |---------------------------------------------------------------------| |
|  |                                                                     | |
|  |  +----------------+  +----------------+  +----------------+         | |
|  |  | Get-SystemInfo |  | Get-DiskInfo   |  | Get-ServiceInfo|         | |
|  |  |----------------|  |----------------|  |----------------|         | |
|  |  | - OS version   |  | - All local    |  | - Critical     |         | |
|  |  | - CPU load     |  |   drives       |  |   services     |         | |
|  |  | - Memory       |  | - Size/free    |  | - Status check |         | |
|  |  | - Uptime       |  | - Percentages  |  +----------------+         | |
|  |  +----------------+  +----------------+                             | |
|  |                                                                     | |
|  |  +----------------+                                                 | |
|  |  | Get-ErrorEvents|                                                 | |
|  |  |----------------|                                                 | |
|  |  | - Last 24h     |                                                 | |
|  |  | - App/System   |                                                 | |
|  |  | - Error/Crit   |                                                 | |
|  |  +----------------+                                                 | |
|  +---------------------------------------------------------------------+ |
|               |                                                          |
|               v                                                          |
|  +---------------------------------------------------------------------+ |
|  |                      STATUS CALCULATOR                              | |
|  |---------------------------------------------------------------------| |
|  | - Apply thresholds to each metric                                   | |
|  | - Determine NORMAL / WARNING / CRITICAL                             | |
|  | - Attach status property to each data object                        | |
|  +----------------------------------+----------------------------------+ |
|                                     |                                    |
|                                     v                                    |
|  +---------------------------------------------------------------------+ |
|  |                       HTML GENERATOR                                | |
|  |---------------------------------------------------------------------| |
|  |                                                                     | |
|  |  +----------------+  +----------------+  +----------------+         | |
|  |  | CSS Styles     |  | HTML Template  |  | Section Builder|         | |
|  |  |----------------|  |----------------|  |----------------|         | |
|  |  | - Table styles |  | - Document     |  | - Summary cards|         | |
|  |  | - Status colors|  |   structure    |  | - Data tables  |         | |
|  |  | - Cards layout |  | - Meta tags    |  | - ConvertTo-   |         | |
|  |  +----------------+  +----------------+  |   Html -Frag.  |         | |
|  |                                          +----------------+         | |
|  +----------------------------------+----------------------------------+ |
|                                     |                                    |
|                                     v                                    |
|  +---------------------------------------------------------------------+ |
|  |                       OUTPUT HANDLER                                | |
|  |---------------------------------------------------------------------| |
|  | - Save HTML file to OutputPath                                      | |
|  | - Open in default browser (if -OpenInBrowser)                       | |
|  | - Send email (if -EmailTo specified)                                | |
|  +---------------------------------------------------------------------+ |
|                                                                          |
+=========================================================================+

Data Collection Strategy

+------------------------------------------------------------------------+
|                     DATA COLLECTION FLOW                                |
+------------------------------------------------------------------------+
|                                                                          |
|  [Script Start]                                                          |
|       |                                                                  |
|       v                                                                  |
|  Parse-Parameters                                                        |
|  (OutputPath, OpenInBrowser, EmailTo, Verbose)                          |
|       |                                                                  |
|       v                                                                  |
|  Initialize-Thresholds                                                   |
|  (Set default warning/critical values)                                   |
|       |                                                                  |
|       v                                                                  |
|  +------------------------------------------------------------------+   |
|  |                  PARALLEL DATA COLLECTION                         |   |
|  |  (In practice, sequential but logically parallel)                |   |
|  |                                                                   |   |
|  |   +--------------------+    Win32_OperatingSystem                |   |
|  |   | Get-SystemMetrics  |--> Win32_Processor                      |   |
|  |   +--------------------+    Win32_ComputerSystem                 |   |
|  |            |                                                      |   |
|  |            v                                                      |   |
|  |   [PSCustomObject] with CPU, Memory, Uptime                      |   |
|  |                                                                   |   |
|  |   +--------------------+    Win32_LogicalDisk (DriveType=3)      |   |
|  |   | Get-DiskMetrics    |--> Calculate percentages                |   |
|  |   +--------------------+    Apply thresholds                     |   |
|  |            |                                                      |   |
|  |            v                                                      |   |
|  |   Array of [PSCustomObject] per drive                            |   |
|  |                                                                   |   |
|  |   +--------------------+    Get-Service (specific list)          |   |
|  |   | Get-ServiceStatus  |--> Check Running/Stopped                |   |
|  |   +--------------------+    Handle "Not Found"                   |   |
|  |            |                                                      |   |
|  |            v                                                      |   |
|  |   Array of [PSCustomObject] per service                          |   |
|  |                                                                   |   |
|  |   +--------------------+    Get-WinEvent (Application, System)   |   |
|  |   | Get-RecentErrors   |--> Filter: Level 1,2 (Crit/Error)       |   |
|  |   +--------------------+    Last 24 hours, max 10                |   |
|  |            |                                                      |   |
|  |            v                                                      |   |
|  |   Array of [PSCustomObject] per event                            |   |
|  |                                                                   |   |
|  +------------------------------------------------------------------+   |
|       |                                                                  |
|       v                                                                  |
|  Combine-AllData                                                         |
|  (Create unified data structure for report)                              |
|       |                                                                  |
|       v                                                                  |
|  Build-HTMLReport                                                        |
|  (Generate complete HTML document)                                       |
|       |                                                                  |
|       v                                                                  |
|  [Output to file / browser / email]                                      |
|                                                                          |
+------------------------------------------------------------------------+

HTML Templating Approach

The report uses a Here-String template with variable interpolation:

$html = @"
<!DOCTYPE html>
<html>
<head>
    <title>System Health - $($Metrics.ComputerName)</title>
    <style>
    $css
    </style>
</head>
<body>
    <h1>System Health Report</h1>
    <p>Computer: <strong>$($Metrics.ComputerName)</strong> |
       Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</p>

    <div class="summary-section">
    $summaryHtml
    </div>

    <h2>Disk Usage</h2>
    $($DiskData | ConvertTo-Html -Fragment)

    <h2>Services</h2>
    $($ServiceData | ConvertTo-Html -Fragment)

    <h2>Recent Errors</h2>
    $errorsHtml
</body>
</html>
"@

Key Techniques:

  1. Here-Strings (@"..."@): Multi-line strings with variable expansion
  2. Subexpressions ($(...)): Execute code within the string
  3. ConvertTo-Html -Fragment: Generate just the <table> portion
  4. String replacement: Add CSS classes for styling after generation

6. Phased Implementation Guide

Phase 1: Basic WMI Queries (2-3 hours)

Goal: Query system information and display in console.

Learning Focus: Understanding CIM classes and their properties.

Steps:

  1. Create a new file Get-SystemHealth.ps1
  2. Query Win32_OperatingSystem for memory and uptime
  3. Query Win32_Processor for CPU info
  4. Query Win32_LogicalDisk for disk space
  5. Display raw values with Write-Host

Starter Code:

#Requires -Version 5.1

# Get basic system information
$os = Get-CimInstance Win32_OperatingSystem
$cpu = Get-CimInstance Win32_Processor

Write-Host "=== System Information ===" -ForegroundColor Cyan
Write-Host "Computer: $($os.CSName)"
Write-Host "OS: $($os.Caption)"
Write-Host "Total Memory (KB): $($os.TotalVisibleMemorySize)"
Write-Host "Free Memory (KB): $($os.FreePhysicalMemory)"
Write-Host "Last Boot: $($os.LastBootUpTime)"
Write-Host ""
Write-Host "CPU: $($cpu.Name)"
Write-Host "Cores: $($cpu.NumberOfCores)"
Write-Host "Load: $($cpu.LoadPercentage)%"
Write-Host ""

# Get disk information
Write-Host "=== Disk Information ===" -ForegroundColor Cyan
Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object {
    Write-Host "Drive $($_.DeviceID): Size=$($_.Size), Free=$($_.FreeSpace)"
}

Verification: Run the script, see raw memory in KB and disk sizes in bytes.


Phase 2: Object Creation and Transformation (1-2 hours)

Goal: Convert raw values to human-readable format using custom objects.

Learning Focus: Calculated properties and PSCustomObject creation.

Steps:

  1. Create a function Get-SystemMetrics that returns a structured object
  2. Convert memory from KB to GB
  3. Convert disk space from bytes to GB
  4. Calculate percentages (used/free)
  5. Format numbers with appropriate precision

Key Code:

function Get-SystemMetrics {
    $os = Get-CimInstance Win32_OperatingSystem
    $cpu = Get-CimInstance Win32_Processor

    # Memory calculations (WMI returns KB, we want GB)
    $totalMemoryGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 2)
    $freeMemoryGB = [math]::Round($os.FreePhysicalMemory / 1MB, 2)
    $usedMemoryGB = $totalMemoryGB - $freeMemoryGB
    $memoryPercent = [math]::Round(($usedMemoryGB / $totalMemoryGB) * 100, 1)

    # Uptime calculation
    $uptime = (Get-Date) - $os.LastBootUpTime
    $uptimeDays = [math]::Round($uptime.TotalDays, 1)

    # Return structured object
    [PSCustomObject]@{
        ComputerName  = $os.CSName
        OSVersion     = $os.Caption
        CPUName       = $cpu.Name
        CPUCores      = $cpu.NumberOfCores
        CPULoad       = $cpu.LoadPercentage
        TotalMemoryGB = $totalMemoryGB
        UsedMemoryGB  = $usedMemoryGB
        FreeMemoryGB  = $freeMemoryGB
        MemoryPercent = $memoryPercent
        UptimeDays    = $uptimeDays
        LastBoot      = $os.LastBootUpTime
    }
}

# Test it
$metrics = Get-SystemMetrics
$metrics | Format-List

Verification: See “8.2 GB used” instead of “8589934592 KB”.


Phase 3: HTML Generation (2-3 hours)

Goal: Create a styled HTML report.

Learning Focus: ConvertTo-Html, CSS styling, Here-Strings.

Steps:

  1. Create CSS stylesheet for professional appearance
  2. Build HTML structure with sections
  3. Use ConvertTo-Html -Fragment for data tables
  4. Combine into complete HTML document
  5. Save to file

CSS Template:

$css = @"
body {
    font-family: 'Segoe UI', Arial, sans-serif;
    margin: 20px;
    background-color: #f5f5f5;
}
h1 {
    color: #333;
    border-bottom: 2px solid #4CAF50;
    padding-bottom: 10px;
}
h2 {
    color: #666;
    margin-top: 30px;
}
table {
    border-collapse: collapse;
    width: 100%;
    margin-top: 10px;
    background-color: white;
    box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}
th {
    background-color: #4CAF50;
    color: white;
    text-align: left;
    padding: 12px;
}
td {
    border: 1px solid #ddd;
    padding: 10px;
}
tr:nth-child(even) { background-color: #f9f9f9; }
tr:hover { background-color: #f1f1f1; }
.critical { background-color: #f8d7da !important; color: #721c24; font-weight: bold; }
.warning { background-color: #fff3cd !important; color: #856404; font-weight: bold; }
.normal { background-color: #d4edda !important; color: #155724; }
.summary-card {
    display: inline-block;
    padding: 15px 25px;
    margin: 10px;
    border-radius: 8px;
    background: white;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    text-align: center;
}
.summary-value { font-size: 2em; font-weight: bold; color: #333; }
.summary-label { color: #666; margin-top: 5px; }
"@

Verification: Open HTML file in browser, see styled tables with data.


Phase 4: Styling and Parameters (1-2 hours)

Goal: Make script configurable and add status indicators.

Learning Focus: param() block, switches, status calculation.

Steps:

  1. Add param() block with -OutputPath and -OpenInBrowser
  2. Implement status threshold logic
  3. Add color-coding based on status
  4. Add comment-based help

Parameter Block:

#Requires -Version 5.1

<#
.SYNOPSIS
    Generates a system health dashboard report.

.DESCRIPTION
    Collects CPU, memory, disk, service, and error information
    and generates an HTML report with color-coded status indicators.

.PARAMETER OutputPath
    Path where the HTML report will be saved.
    Default: $env:TEMP\SystemHealth_<timestamp>.html

.PARAMETER OpenInBrowser
    If specified, opens the report in the default browser.
    Default: $true

.PARAMETER EmailTo
    Email address to send the report to.

.EXAMPLE
    .\Get-SystemHealth.ps1
    Generates report and opens in browser.

.EXAMPLE
    .\Get-SystemHealth.ps1 -OutputPath "C:\Reports\health.html" -OpenInBrowser:$false
    Generates report to specific path without opening.
#>

param(
    [Parameter(Mandatory = $false)]
    [string]$OutputPath = "$env:TEMP\SystemHealth_$(Get-Date -Format 'yyyyMMdd_HHmmss').html",

    [Parameter(Mandatory = $false)]
    [switch]$OpenInBrowser = $true,

    [Parameter(Mandatory = $false)]
    [string]$EmailTo
)

Status Calculation Function:

function Get-Status {
    param(
        [double]$Value,
        [double]$WarningThreshold,
        [double]$CriticalThreshold
    )

    if ($Value -ge $CriticalThreshold) {
        return "CRITICAL"
    }
    elseif ($Value -ge $WarningThreshold) {
        return "WARNING"
    }
    else {
        return "NORMAL"
    }
}

7. Testing Strategy

Unit Tests

Test Case Input Expected Output How to Verify
Get-SystemMetrics returns object None PSCustomObject with all properties $m = Get-SystemMetrics; $m.GetType().Name -eq 'PSCustomObject'
Memory conversion accurate Raw KB value GB value / 1048576 Compare manual calculation
CPU load retrieves value None Integer 0-100 or $null $m.CPULoad -ge 0 -and $m.CPULoad -le 100
Disk metrics returns array None Array of disk objects (Get-DiskMetrics).Count -ge 1
Status normal 50%, thresholds 80/95 “NORMAL” Get-Status -Value 50 -Warning 80 -Critical 95
Status warning 85%, thresholds 80/95 “WARNING” Get-Status -Value 85 -Warning 80 -Critical 95
Status critical 96%, thresholds 80/95 “CRITICAL” Get-Status -Value 96 -Warning 80 -Critical 95

Integration Tests

Test Case Steps Expected Result
Full report generation Run .\Get-SystemHealth.ps1 HTML file created at default path
Custom output path Run with -OutputPath "C:\test.html" File created at specified path
Browser auto-open Run with -OpenInBrowser Default browser opens with report
No browser open Run with -OpenInBrowser:$false Report saved but browser not opened
Service detection Include existing and non-existing services Existing shows status, non-existing shows “Not Installed”
Error log access Run on system with errors Errors displayed in report
Error log empty Run on clean system “No errors found” message

Edge Cases

Test Case Scenario Expected Handling
No local disks Virtual machine with no local storage Empty disk table or informative message
No admin rights Standard user account Complete with available data, skip restricted queries
WMI timeout Slow system or corrupted WMI Catch exception, return partial data
Very long uptime System running 500+ days Warning/Critical status correctly applied
100% disk full Drive at capacity Critical status, no division by zero
No recent errors No events matching criteria “No errors in last 24 hours” message
Invalid output path Non-existent directory Create directory or show clear error

Pester Test Example

Describe "Get-SystemHealth" {
    BeforeAll {
        . $PSScriptRoot\Get-SystemHealth.ps1
    }

    Context "Get-SystemMetrics" {
        It "Returns a PSCustomObject" {
            $result = Get-SystemMetrics
            $result | Should -BeOfType [PSCustomObject]
        }

        It "Contains expected properties" {
            $result = Get-SystemMetrics
            $result.PSObject.Properties.Name | Should -Contain 'ComputerName'
            $result.PSObject.Properties.Name | Should -Contain 'CPULoad'
            $result.PSObject.Properties.Name | Should -Contain 'MemoryPercent'
        }

        It "Memory values are within valid range" {
            $result = Get-SystemMetrics
            $result.MemoryPercent | Should -BeGreaterOrEqual 0
            $result.MemoryPercent | Should -BeLessOrEqual 100
        }
    }

    Context "Get-Status" {
        It "Returns NORMAL for values below warning" {
            Get-Status -Value 50 -WarningThreshold 80 -CriticalThreshold 95 |
                Should -Be "NORMAL"
        }

        It "Returns WARNING for values at or above warning" {
            Get-Status -Value 80 -WarningThreshold 80 -CriticalThreshold 95 |
                Should -Be "WARNING"
        }

        It "Returns CRITICAL for values at or above critical" {
            Get-Status -Value 95 -WarningThreshold 80 -CriticalThreshold 95 |
                Should -Be "CRITICAL"
        }
    }
}

8. Common Pitfalls and Debugging Tips

Problem: CIM Query Returns Nothing

Symptoms:

  • Get-CimInstance returns empty result
  • No error message displayed

Possible Causes:

  1. Filter syntax error (wrong property name)
  2. Class name misspelled
  3. WMI repository corruption
  4. Querying remote machine without access

Solutions:

# 1. Test query without filter first
Get-CimInstance Win32_LogicalDisk  # Does this return anything?

# 2. Verify class exists
Get-CimClass -ClassName Win32_LogicalDisk

# 3. Check available properties
Get-CimInstance Win32_LogicalDisk | Get-Member

# 4. If WMI is corrupted, rebuild repository (admin required)
# winmgmt /salvagerepository
# winmgmt /resetrepository

# 5. Try legacy WMI as fallback
Get-WmiObject Win32_LogicalDisk -Filter "DriveType=3"

Problem: HTML Doesn’t Render Correctly

Symptoms:

  • Garbled text in browser
  • Tables not styled
  • Missing sections

Possible Causes:

  1. Unclosed HTML tags
  2. Special characters not escaped
  3. Encoding issues (UTF-8 vs. ANSI)
  4. Here-String syntax error

Solutions:

# 1. Validate HTML (open browser developer tools, check console for errors)

# 2. Escape special characters in data
[System.Web.HttpUtility]::HtmlEncode($unsafeString)

# Or in PowerShell 7+:
[System.Net.WebUtility]::HtmlEncode($unsafeString)

# 3. Always specify UTF-8 encoding
$html | Out-File -FilePath $path -Encoding UTF8

# 4. Check here-string syntax (no spaces before closing @")
$html = @"
content here
"@  # This line must start at column 0, no leading spaces

Problem: Event Log Access Denied

Symptoms:

  • Get-WinEvent throws “Access denied”
  • Only happens on certain logs

Possible Causes:

  1. Security log requires admin
  2. Other logs may have custom ACLs
  3. Remote event log access needs configuration

Solutions:

# 1. Wrap in try/catch and provide fallback
try {
    $errors = Get-WinEvent -FilterHashtable @{
        LogName = 'Application', 'System'
        Level = 1, 2
        StartTime = (Get-Date).AddHours(-24)
    } -MaxEvents 10 -ErrorAction Stop
}
catch {
    Write-Warning "Could not access event logs: $_"
    $errors = @()  # Return empty array
}

# 2. Check which logs you CAN access
Get-WinEvent -ListLog * | Where-Object { $_.RecordCount -gt 0 }

# 3. Document requirement: "Run as Administrator for full event access"

Problem: CPU Load Returns $null

Symptoms:

  • Win32_Processor.LoadPercentage is $null
  • Only on certain systems

Possible Causes:

  1. WMI performance counter not initialized
  2. First query after boot returns $null
  3. Multi-CPU systems return array

Solutions:

# 1. Query twice (first initializes, second returns value)
$null = Get-CimInstance Win32_Processor
Start-Sleep -Milliseconds 100
$cpu = Get-CimInstance Win32_Processor

# 2. Use performance counters instead (more reliable)
$cpuCounter = Get-Counter '\Processor(_Total)\% Processor Time'
$cpuLoad = [math]::Round($cpuCounter.CounterSamples.CookedValue, 1)

# 3. Handle array for multi-CPU (take average)
$cpus = Get-CimInstance Win32_Processor
$cpuLoad = ($cpus | Measure-Object -Property LoadPercentage -Average).Average

Problem: Script Takes Too Long

Symptoms:

  • Script runs for 30+ seconds
  • User perceives it as frozen

Possible Causes:

  1. WMI queries are slow
  2. Remote CIM sessions timing out
  3. Event log query returning too many results

Solutions:

# 1. Add progress indicators
Write-Host "Collecting CPU data..." -ForegroundColor Cyan
$cpu = Get-CimInstance Win32_Processor

# 2. Use -MaxEvents for event logs
Get-WinEvent -FilterHashtable @{...} -MaxEvents 10  # Limit results

# 3. Reduce event log time window
$startTime = (Get-Date).AddHours(-6)  # Last 6 hours instead of 24

# 4. Consider parallel collection in PowerShell 7
$results = @()
@('Win32_OperatingSystem', 'Win32_Processor', 'Win32_LogicalDisk') |
    ForEach-Object -Parallel {
        Get-CimInstance $_
    } -ThrottleLimit 3

Problem: File Path Issues

Symptoms:

  • “Path not found” errors
  • File saved in wrong location
  • Special characters in path cause problems

Solutions:

# 1. Always use fully qualified paths
$OutputPath = [System.IO.Path]::GetFullPath($OutputPath)

# 2. Create parent directory if needed
$directory = [System.IO.Path]::GetDirectoryName($OutputPath)
if (-not (Test-Path $directory)) {
    New-Item -ItemType Directory -Path $directory -Force
}

# 3. Handle paths with spaces
"$OutputPath"  # Always quote variables

# 4. Validate path before using
if (-not (Test-Path -IsValid $OutputPath)) {
    throw "Invalid path: $OutputPath"
}

9. Extensions and Challenges

Easy Extensions

Extension Description Skills Practiced
Custom thresholds Add -CPUWarning, -MemoryWarning parameters Parameter handling, validation
JSON output Add -OutputFormat parameter (HTML/JSON) ConvertTo-Json, format switching
Timestamped filenames Auto-generate unique names with date/time String formatting, file handling
Custom service list Read monitored services from config file File I/O, configuration patterns
Dark mode CSS Alternative styling for dark theme CSS, parameter switches

Medium Extensions

Extension Description Skills Practiced
Historical trending Store metrics over time, show graphs Data storage, visualization
Scheduled execution Create/manage Task Scheduler jobs Windows API, scheduled tasks
Email on threshold Only email when warnings/criticals exist Conditional logic, SMTP
Multiple computers Accept -ComputerName array parameter Remote CIM, error handling
Network metrics Add network adapter utilization Additional WMI classes
Process monitoring Top 10 CPU/memory consuming processes Process queries, sorting

Advanced Extensions

Extension Description Skills Practiced
PDF export Convert HTML to PDF using Chromium External tools, process management
Live dashboard Web server with auto-refresh HTTP listener, async
Multi-tenant Central server collects from many machines Remoting, aggregation
Grafana integration Send metrics to Prometheus/Grafana Metrics format, HTTP APIs
Alert webhooks Send to Slack/Teams on critical REST APIs, JSON payloads
Compare baselines Compare current state to saved baseline Data comparison, diffing

Challenge Projects

Challenge 1: Predictive Disk Warning

  • Track disk usage over time
  • Calculate daily growth rate
  • Predict when disk will fill up
  • Alert X days before capacity

Challenge 2: Dependency Service Monitoring

  • Query service dependencies
  • Build dependency graph
  • Warn if critical dependency is stopped
  • Visualize in HTML

Challenge 3: Cross-Platform Health

  • Make script work on Linux (PowerShell Core)
  • Abstract OS-specific queries
  • Use /proc filesystem on Linux
  • Unified output format

10. Books That Will Help

Topic Book Relevant Chapter Why It Helps
WMI/CIM fundamentals Learn PowerShell in a Month of Lunches (4th Ed.) by Travis Plunk & James Petty Ch. 8: “Objects: Data by another name” Explains object-based thinking and WMI queries
Pipeline mastery Learn PowerShell in a Month of Lunches Ch. 9: “The pipeline: Connecting commands” Deep dive into pipeline mechanics
Where/Select/ForEach Learn PowerShell in a Month of Lunches Ch. 10: “Filtering and comparisons” Filtering and projection patterns
Error handling Learn PowerShell in a Month of Lunches Ch. 18: “Error handling” Try/catch/finally best practices
Advanced functions Windows PowerShell in Action (3rd Ed.) by Bruce Payette Ch. 7: “PowerShell functions” Parameter blocks, validation, help
CIM deep dive Windows PowerShell in Action Ch. 19: “Management objects: WMI and CIM” Comprehensive WMI/CIM coverage
Practical recipes The PowerShell Cookbook (4th Ed.) by Lee Holmes Various sections Ready-to-use solutions for common tasks
HTML/report generation PowerShell for Sysadmins by Adam Bertram Ch. 10: “Building Reusable Tools” Creating professional output
Remoting Learn PowerShell in a Month of Lunches Ch. 11-13 Remote execution for multi-machine
Module development The Pester Book by Adam Bertram Testing chapters Writing testable, modular code
  1. Start with: “Learn PowerShell in a Month of Lunches” - Ch. 8, 9, 10
    • Foundation for understanding objects and pipelines
  2. Then: “Learn PowerShell in a Month of Lunches” - Ch. 18
    • Error handling is critical for robust scripts
  3. Reference as needed: “The PowerShell Cookbook”
    • Look up specific techniques when you need them
  4. For deeper understanding: “Windows PowerShell in Action” - Ch. 19
    • Comprehensive WMI/CIM reference

11. Self-Assessment Checklist

Before considering this project complete, verify each item:

Core Functionality

  • Script queries CPU usage from Win32_Processor
  • Script queries memory usage from Win32_OperatingSystem
  • Script queries disk space from Win32_LogicalDisk (local drives only)
  • System uptime is calculated correctly from LastBootUpTime
  • Values are converted to human-readable format (GB, percentages)
  • Status thresholds work correctly (NORMAL/WARNING/CRITICAL)

HTML Report

  • HTML report is generated with valid structure
  • CSS styling is applied (tables, colors, layout)
  • Status values are color-coded (green/yellow/red)
  • Summary cards show key metrics at a glance
  • Report includes timestamp and computer name
  • Report opens correctly in major browsers (Chrome, Edge, Firefox)

Additional Features

  • Service status section shows configured services
  • Missing services are handled gracefully (“Not Installed”)
  • Recent errors section shows last 24 hours of errors
  • Empty error log shows appropriate message

Parameters and Robustness

  • -OutputPath parameter works for custom save location
  • -OpenInBrowser switch controls browser auto-open
  • Script handles errors gracefully with try/catch
  • Script works without administrator privileges (limited data)
  • Script completes in under 10 seconds
  • Comment-based help is included (.SYNOPSIS, .EXAMPLE)

Code Quality

  • Code is organized into logical functions
  • Variable and function names are descriptive
  • Magic numbers are replaced with named variables/thresholds
  • Code follows PowerShell naming conventions (Verb-Noun)
  • No hardcoded paths (uses $env:TEMP, parameters)

Edge Cases

  • Handles systems with single CPU
  • Handles systems with multiple CPUs
  • Handles disks at 100% capacity without errors
  • Handles systems with no recent errors
  • Works on Windows 10 and Windows 11
  • Works with PowerShell 5.1 and PowerShell 7+

Complete Solution Reference

Below is a complete, production-ready implementation that you can reference after attempting each phase yourself.

Full Script: Get-SystemHealth.ps1

#Requires -Version 5.1

<#
.SYNOPSIS
    Generates a system health dashboard report.

.DESCRIPTION
    Collects CPU, memory, disk, service, and error information
    and generates an HTML report with color-coded status indicators.

.PARAMETER OutputPath
    Path where the HTML report will be saved.
    Default: $env:TEMP\SystemHealth_<timestamp>.html

.PARAMETER OpenInBrowser
    If specified, opens the report in the default browser.
    Default: $true

.PARAMETER EmailTo
    Email address to send the report to (not implemented in basic version).

.EXAMPLE
    .\Get-SystemHealth.ps1
    Generates report and opens in browser.

.EXAMPLE
    .\Get-SystemHealth.ps1 -OutputPath "C:\Reports\health.html" -OpenInBrowser:$false
    Generates report to specific path without opening.
#>

param(
    [Parameter(Mandatory = $false)]
    [string]$OutputPath = "$env:TEMP\SystemHealth_$(Get-Date -Format 'yyyyMMdd_HHmmss').html",

    [Parameter(Mandatory = $false)]
    [switch]$OpenInBrowser = $true,

    [Parameter(Mandatory = $false)]
    [string]$EmailTo
)

#region Configuration

$Thresholds = @{
    CPU = @{
        Warning  = 80
        Critical = 95
    }
    Memory = @{
        Warning  = 75
        Critical = 90
    }
    Disk = @{
        Warning  = 80
        Critical = 95
    }
    Uptime = @{
        Warning  = 90
        Critical = 180
    }
}

$CriticalServices = @(
    'wuauserv',      # Windows Update
    'WinDefend',     # Windows Defender
    'EventLog',      # Windows Event Log
    'Spooler',       # Print Spooler
    'MSSQLSERVER'    # SQL Server (if installed)
)

#endregion

#region Helper Functions

function Get-Status {
    param(
        [double]$Value,
        [double]$WarningThreshold,
        [double]$CriticalThreshold
    )

    if ($Value -ge $CriticalThreshold) {
        return "CRITICAL"
    }
    elseif ($Value -ge $WarningThreshold) {
        return "WARNING"
    }
    else {
        return "NORMAL"
    }
}

function Get-SystemMetrics {
    $os = Get-CimInstance Win32_OperatingSystem
    $cpu = Get-CimInstance Win32_Processor

    # Memory calculations (WMI returns KB)
    $totalMemoryGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 2)
    $freeMemoryGB = [math]::Round($os.FreePhysicalMemory / 1MB, 2)
    $usedMemoryGB = $totalMemoryGB - $freeMemoryGB
    $memoryPercent = [math]::Round(($usedMemoryGB / $totalMemoryGB) * 100, 1)

    # Uptime calculation
    $uptime = (Get-Date) - $os.LastBootUpTime
    $uptimeDays = [math]::Round($uptime.TotalDays, 1)

    # Handle multi-CPU systems
    $cpuLoad = if ($cpu -is [array]) {
        ($cpu | Measure-Object -Property LoadPercentage -Average).Average
    }
    else {
        $cpu.LoadPercentage
    }
    $cpuLoad = if ($null -eq $cpuLoad) { 0 } else { [int]$cpuLoad }

    $cpuName = if ($cpu -is [array]) { $cpu[0].Name } else { $cpu.Name }
    $cpuCores = if ($cpu -is [array]) {
        ($cpu | Measure-Object -Property NumberOfCores -Sum).Sum
    }
    else {
        $cpu.NumberOfCores
    }

    [PSCustomObject]@{
        ComputerName   = $os.CSName
        OSVersion      = $os.Caption
        CPUName        = $cpuName.Trim()
        CPUCores       = $cpuCores
        CPULoad        = $cpuLoad
        CPUStatus      = Get-Status -Value $cpuLoad -WarningThreshold $Thresholds.CPU.Warning -CriticalThreshold $Thresholds.CPU.Critical
        TotalMemoryGB  = $totalMemoryGB
        UsedMemoryGB   = $usedMemoryGB
        FreeMemoryGB   = $freeMemoryGB
        MemoryPercent  = $memoryPercent
        MemoryStatus   = Get-Status -Value $memoryPercent -WarningThreshold $Thresholds.Memory.Warning -CriticalThreshold $Thresholds.Memory.Critical
        UptimeDays     = $uptimeDays
        UptimeStatus   = Get-Status -Value $uptimeDays -WarningThreshold $Thresholds.Uptime.Warning -CriticalThreshold $Thresholds.Uptime.Critical
        LastBoot       = $os.LastBootUpTime
    }
}

function Get-DiskMetrics {
    Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object {
        $sizeGB = [math]::Round($_.Size / 1GB, 2)
        $freeGB = [math]::Round($_.FreeSpace / 1GB, 2)
        $usedGB = $sizeGB - $freeGB
        $usedPercent = if ($sizeGB -gt 0) {
            [math]::Round(($usedGB / $sizeGB) * 100, 1)
        }
        else { 0 }

        [PSCustomObject]@{
            Drive       = $_.DeviceID
            Label       = $_.VolumeName
            SizeGB      = $sizeGB
            UsedGB      = $usedGB
            FreeGB      = $freeGB
            UsedPercent = $usedPercent
            Status      = Get-Status -Value $usedPercent -WarningThreshold $Thresholds.Disk.Warning -CriticalThreshold $Thresholds.Disk.Critical
        }
    }
}

function Get-ServiceStatus {
    $results = @()

    foreach ($serviceName in $CriticalServices) {
        try {
            $service = Get-Service -Name $serviceName -ErrorAction Stop

            $status = if ($service.Status -eq 'Running') {
                "NORMAL"
            }
            else {
                "WARNING"
            }

            $results += [PSCustomObject]@{
                Name         = $service.DisplayName
                ServiceName  = $service.Name
                State        = $service.Status.ToString()
                HealthStatus = $status
            }
        }
        catch {
            $results += [PSCustomObject]@{
                Name         = $serviceName
                ServiceName  = $serviceName
                State        = "Not Installed"
                HealthStatus = "INFO"
            }
        }
    }

    return $results
}

function Get-RecentErrors {
    param(
        [int]$Hours = 24,
        [int]$MaxEvents = 10
    )

    $startTime = (Get-Date).AddHours(-$Hours)

    try {
        Get-WinEvent -FilterHashtable @{
            LogName   = 'Application', 'System'
            Level     = 1, 2  # Critical = 1, Error = 2
            StartTime = $startTime
        } -MaxEvents $MaxEvents -ErrorAction Stop | ForEach-Object {
            $msgLength = if ($_.Message.Length -gt 100) { 100 } else { $_.Message.Length }
            [PSCustomObject]@{
                Time    = $_.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss")
                Level   = $_.LevelDisplayName
                Source  = $_.ProviderName
                Message = $_.Message.Substring(0, $msgLength) + "..."
            }
        }
    }
    catch {
        Write-Warning "Could not retrieve event logs: $_"
        return @()
    }
}

function New-HTMLReport {
    param(
        [object]$SystemMetrics,
        [object[]]$DiskMetrics,
        [object[]]$ServiceStatus,
        [object[]]$RecentErrors
    )

    $css = @"
<style>
    * { box-sizing: border-box; }
    body {
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        margin: 0;
        padding: 20px;
        background-color: #f0f2f5;
        color: #333;
    }
    .container {
        max-width: 1200px;
        margin: 0 auto;
    }
    h1 {
        color: #2c3e50;
        border-bottom: 3px solid #3498db;
        padding-bottom: 10px;
        margin-bottom: 5px;
    }
    .timestamp {
        color: #7f8c8d;
        margin-bottom: 20px;
    }
    h2 {
        color: #34495e;
        margin-top: 30px;
        padding-bottom: 5px;
        border-bottom: 1px solid #bdc3c7;
    }
    .summary-container {
        display: flex;
        flex-wrap: wrap;
        gap: 15px;
        margin: 20px 0;
    }
    .summary-card {
        background: white;
        border-radius: 10px;
        padding: 20px 30px;
        text-align: center;
        box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        min-width: 150px;
        flex: 1;
    }
    .summary-value {
        font-size: 2.5em;
        font-weight: bold;
        color: #2c3e50;
    }
    .summary-label {
        color: #7f8c8d;
        margin-top: 5px;
        font-size: 0.9em;
    }
    .summary-status {
        margin-top: 10px;
        padding: 3px 10px;
        border-radius: 15px;
        font-size: 0.8em;
        font-weight: bold;
    }
    table {
        border-collapse: collapse;
        width: 100%;
        margin-top: 15px;
        background-color: white;
        box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        border-radius: 8px;
        overflow: hidden;
    }
    th {
        background-color: #3498db;
        color: white;
        text-align: left;
        padding: 15px;
        font-weight: 600;
    }
    td {
        border-bottom: 1px solid #ecf0f1;
        padding: 12px 15px;
    }
    tr:last-child td { border-bottom: none; }
    tr:hover { background-color: #f8f9fa; }
    .CRITICAL, .critical {
        background-color: #fadbd8 !important;
        color: #922b21;
        font-weight: bold;
    }
    .WARNING, .warning {
        background-color: #fdebd0 !important;
        color: #9c640c;
        font-weight: bold;
    }
    .NORMAL, .normal {
        background-color: #d5f5e3 !important;
        color: #1e8449;
    }
    .INFO, .info {
        background-color: #d6eaf8 !important;
        color: #1a5276;
    }
    .info-table {
        width: 100%;
    }
    .info-table td:first-child {
        font-weight: 600;
        width: 200px;
        color: #7f8c8d;
    }
    .footer {
        margin-top: 40px;
        padding-top: 20px;
        border-top: 1px solid #bdc3c7;
        text-align: center;
        color: #95a5a6;
        font-size: 0.9em;
    }
</style>
"@

    # Summary cards
    $cpuStatusClass = $SystemMetrics.CPUStatus.ToLower()
    $memStatusClass = $SystemMetrics.MemoryStatus.ToLower()
    $uptimeStatusClass = $SystemMetrics.UptimeStatus.ToLower()

    $summaryHtml = @"
<div class="summary-container">
    <div class="summary-card">
        <div class="summary-value">$($SystemMetrics.CPULoad)%</div>
        <div class="summary-label">CPU Usage</div>
        <div class="summary-status $cpuStatusClass">$($SystemMetrics.CPUStatus)</div>
    </div>
    <div class="summary-card">
        <div class="summary-value">$($SystemMetrics.MemoryPercent)%</div>
        <div class="summary-label">Memory Used</div>
        <div class="summary-status $memStatusClass">$($SystemMetrics.MemoryStatus)</div>
    </div>
    <div class="summary-card">
        <div class="summary-value">$($SystemMetrics.UptimeDays)</div>
        <div class="summary-label">Days Uptime</div>
        <div class="summary-status $uptimeStatusClass">$($SystemMetrics.UptimeStatus)</div>
    </div>
</div>
"@

    # System info table
    $sysInfoHtml = @"
<table class="info-table">
    <tr><td>Operating System</td><td>$($SystemMetrics.OSVersion)</td></tr>
    <tr><td>CPU</td><td>$($SystemMetrics.CPUName) ($($SystemMetrics.CPUCores) cores)</td></tr>
    <tr><td>Total Memory</td><td>$($SystemMetrics.TotalMemoryGB) GB</td></tr>
    <tr><td>Used Memory</td><td>$($SystemMetrics.UsedMemoryGB) GB ($($SystemMetrics.MemoryPercent)%)</td></tr>
    <tr><td>Last Boot</td><td>$($SystemMetrics.LastBoot.ToString('yyyy-MM-dd HH:mm:ss'))</td></tr>
</table>
"@

    # Disk table
    $diskHtml = $DiskMetrics |
        Select-Object Drive, Label,
            @{Name = 'Size (GB)'; Expression = { $_.SizeGB } },
            @{Name = 'Used (GB)'; Expression = { $_.UsedGB } },
            @{Name = 'Free (GB)'; Expression = { $_.FreeGB } },
            @{Name = 'Used %'; Expression = { "$($_.UsedPercent)%" } },
            Status |
        ConvertTo-Html -Fragment

    $diskHtml = $diskHtml -replace '<td>CRITICAL</td>', '<td class="CRITICAL">CRITICAL</td>'
    $diskHtml = $diskHtml -replace '<td>WARNING</td>', '<td class="WARNING">WARNING</td>'
    $diskHtml = $diskHtml -replace '<td>NORMAL</td>', '<td class="NORMAL">NORMAL</td>'

    # Service table
    $serviceHtml = $ServiceStatus |
        Select-Object Name, State, HealthStatus |
        ConvertTo-Html -Fragment

    $serviceHtml = $serviceHtml -replace '<td>CRITICAL</td>', '<td class="CRITICAL">CRITICAL</td>'
    $serviceHtml = $serviceHtml -replace '<td>WARNING</td>', '<td class="WARNING">WARNING</td>'
    $serviceHtml = $serviceHtml -replace '<td>NORMAL</td>', '<td class="NORMAL">NORMAL</td>'
    $serviceHtml = $serviceHtml -replace '<td>INFO</td>', '<td class="INFO">INFO</td>'

    # Errors section
    $errorsHtml = if ($RecentErrors.Count -gt 0) {
        $RecentErrors | ConvertTo-Html -Fragment
    }
    else {
        "<p style='color: #27ae60; font-weight: bold;'>No errors found in the last 24 hours.</p>"
    }

    # Complete HTML
    $html = @"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>System Health Report - $($SystemMetrics.ComputerName)</title>
    $css
</head>
<body>
    <div class="container">
        <h1>System Health Report</h1>
        <p class="timestamp">
            <strong>Computer:</strong> $($SystemMetrics.ComputerName) |
            <strong>Generated:</strong> $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
        </p>

        $summaryHtml

        <h2>System Information</h2>
        $sysInfoHtml

        <h2>Disk Usage</h2>
        $diskHtml

        <h2>Critical Services</h2>
        $serviceHtml

        <h2>Recent Errors (Last 24 Hours)</h2>
        $errorsHtml

        <div class="footer">
            <p>Report generated by Get-SystemHealth.ps1 | PowerShell $($PSVersionTable.PSVersion)</p>
        </div>
    </div>
</body>
</html>
"@

    return $html
}

#endregion

#region Main Execution

try {
    Write-Host "Collecting system metrics..." -ForegroundColor Cyan

    Write-Host "  [..] CPU and Memory" -NoNewline
    $systemMetrics = Get-SystemMetrics
    Write-Host "`r  [OK] CPU and Memory" -ForegroundColor Green

    Write-Host "  [..] Disk Information" -NoNewline
    $diskMetrics = Get-DiskMetrics
    Write-Host "`r  [OK] Disk Information" -ForegroundColor Green

    Write-Host "  [..] Service Status" -NoNewline
    $serviceStatus = Get-ServiceStatus
    Write-Host "`r  [OK] Service Status" -ForegroundColor Green

    Write-Host "  [..] Recent Errors" -NoNewline
    $recentErrors = Get-RecentErrors
    $errorCount = if ($recentErrors) { $recentErrors.Count } else { 0 }
    Write-Host "`r  [OK] Recent Errors ($errorCount found)" -ForegroundColor Green

    Write-Host "Generating HTML report..." -ForegroundColor Cyan

    $html = New-HTMLReport -SystemMetrics $systemMetrics `
        -DiskMetrics $diskMetrics `
        -ServiceStatus $serviceStatus `
        -RecentErrors $recentErrors

    # Ensure output directory exists
    $outputDir = [System.IO.Path]::GetDirectoryName($OutputPath)
    if ($outputDir -and -not (Test-Path $outputDir)) {
        New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
    }

    # Save to file
    $html | Out-File -FilePath $OutputPath -Encoding UTF8

    Write-Host "Report saved to: $OutputPath" -ForegroundColor Green

    # Open in browser if requested
    if ($OpenInBrowser) {
        Write-Host "Opening in browser..." -ForegroundColor Cyan
        Start-Process $OutputPath
    }

    # Return the path for script consumers
    return $OutputPath
}
catch {
    Write-Error "Error generating report: $_"
    exit 1
}

#endregion

Interview Preparation

Questions You Should Be Able to Answer

1. “What’s the difference between WMI and CIM?”

CIM (Common Information Model) is the modern standard for querying Windows system information, using WS-Man protocol (same as WinRM). WMI is the legacy implementation using DCOM. CIM is faster, more secure, and firewall-friendly. Always use Get-CimInstance for new scripts; Get-WmiObject is deprecated in PowerShell Core.

2. “Explain how the PowerShell pipeline works.”

PowerShell’s pipeline passes objects, not text. Each cmdlet receives objects from the previous cmdlet and emits objects to the next. Objects have typed properties accessed by name, not position. This enables filtering (Where-Object), projection (Select-Object), and transformation without string parsing.

3. “How would you handle errors in this script?”

Use try/catch/finally blocks around operations that might fail. Set -ErrorAction Stop on cmdlets to make non-terminating errors catchable. Provide meaningful fallback behavior (empty arrays, default values) when queries fail. Log warnings for partial failures but continue execution.

4. “How would you scale this to 100 servers?”

Use Invoke-Command -ComputerName $servers -ScriptBlock {...} for parallel remote execution. Implement -ComputerName parameter accepting an array. Handle individual server failures gracefully without stopping the entire collection. Aggregate results and generate a combined report or individual reports per server.

5. “Why use calculated properties instead of transforming data afterward?”

Calculated properties transform data at query time, reducing memory usage and processing steps. They make the pipeline self-documenting. They allow renaming properties for cleaner output. The alternative (ForEach-Object transformation) works but is more verbose and separates the transformation logic from the query.