Creating HTML Reports with PowerShell Part 3
Let's continue our exploration of HTML reporting with PowerShell. I know I promised some alternatives to ConvertTo-Html
, and I will get to them. But before that, I want to dive a little deeper into styling. As I showed you in previous newsletters, you will use CSS for styling your reports.
This works pretty well for styling broad HTML elements like a table or the document body. If you are using fragments, you can insert a style class.
Advanced Style Techniques
Here's an example. I am adding a metadata footer to my report.
$fragments += @"
<p>
Report run : $((Get-Date).ToUniversalTime()) UTC by $($env:UserDomain)\$($env:UserName)
<br>
Report script : $(Convert-Path $($MyInvocation.InvocationName))
<br>
Report version: $ver
<br>
Report source : $env:ComputerName
</p>
"@
I would like to style this footer. In my CSS settings, I can define a custom class.
.footer {
color: rgb(118, 160, 118);
margin-left: 25px;
font-family: Verdana, Tahoma, Arial, sans-serif;
font-style: italic;
}
I am far from a CSS expert, but I that you should define the custom class with a period prefix. VS Code helped me with the settings. In my script, I can then apply the class to the footer paragraph.
$fragments += @"
<p class='footer'>
Report run : $((Get-Date).ToUniversalTime()) UTC by $($env:UserDomain)\$($env:UserName)
<br>
Report script : $(Convert-Path $($MyInvocation.InvocationName))
<br>
Report version: $ver
<br>
Report source : $env:ComputerName
</p>
"@
Notice that you don't need to use the period. To be on the safe side, I recommend using a lowercase name.
I have a variation on the system status report script. This version relies on an external CSS file.
body {
background-color:rgb(255, 255, 200);
font-family: Monospace;
font-size: 12pt;
}
td,th {
border: 0px solid black;
border-collapse: collapse;
white-space: pre;
padding: 10px;
}
th {
color: white;
background-color: black;
}
table,tr,td,th {
margin: 0px;
white-space: pre;
}
tr:nth-child(odd) {
background-color: LightGray
}
td {
white-space: break-spaces;
word-wrap: break-word;
}
table {
margin-left: 25px;
table-layout: auto;
}
h2 {
font-family: Tahoma;
color:blue;
}
.footer {
color: rgb(118, 160, 118);
margin-left: 25px;
font-family: Verdana, Tahoma, Arial, sans-serif;
font-style: italic;
}
In the script, I pass the CSS file as a parameter.
#parameters should be updated with validation and parameter attributes
param(
[string[]]$ComputerName = $env:ComputerName,
[string]$FilePath = 'C:\temp\SysReport.html',
[PSCredential]$Credential,
[string]$CSSFile = "SysReport.css",
[switch]$Passthru
)
In the script, Import the CSS file and use it in the HTML header.
#import style from external file
$style = Get-Content -Path $CSSFile -Raw
$head = @"
<head>
<Title>System Status Report</Title>
<style>
$style
</style>
</head>
"@
The report is styled accordingly.

Even my custom footer.

Here's the script if you want to try it out. You'll want to grab the CSS file and save it to the same directory.
#requires -version 5.1
#requires -modules Microsoft.PowerShell.ThreadJob,CimCmdlets
#SysReport.ps1
#parameters should be updated with validation and parameter attributes
param(
[string[]]$ComputerName = $env:ComputerName,
[string]$FilePath = 'C:\temp\SysReport.html',
[PSCredential]$Credential,
[string]$CSSFile = "SysReport.css",
[switch]$Passthru
)
#script version
$ver = '1.4.0'
Clear-Host
Write-Host "Starting $($MyInvocation.MyCommand)" -ForegroundColor Cyan
#initialize an array for all jobs
$Jobs = @()
#remove FilePath from PSBoundParameters
[Void]$PSBoundParameters.Remove('FilePath')
[Void]$PSBoundParameters.Remove('Passthru')
Write-Host "Creating $($ComputerName.Count) CimSession(s)" -ForegroundColor Cyan
$cimSess = New-CimSession @PSBoundParameters
#OS Info
$os = {
param($CimSession)
Get-CimInstance -ClassName Win32_OperatingSystem -CimSession $CimSession |
Select-Object -Property @{Name = 'ComputerName'; Expression = { $_.CSName } },
@{Name = 'OSName'; Expression = { $_.Caption } }, Version, OSArchitecture, LastBootUpTime,
@{Name= 'PctFreeMemory';Expression = {'{0:p2}' -f (($_.FreePhysicalMemory / $_.TotalVisibleMemorySize))}},
@{Name = 'Uptime'; Expression = { New-TimeSpan -Start $_.LastBootUpTime -End (Get-Date) } },
@{Name = 'ReportType'; Expression = { 'OSInfo' } }
}
Write-Host "Starting $($CimSess.count) OSInfo job(s)" -ForegroundColor Cyan
$cimSess | ForEach-Object {
#jobs are created in the script scope
$Jobs += Start-ThreadJob -ScriptBlock $os -ArgumentList $_ -Name OSInfo
}
#Disk space
$disk = {
param($CimSession)
Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -CimSession $CimSession |
Select-Object -Property DeviceID,
@{Name = 'ComputerName'; Expression = { $_.SystemName } },
@{Name = 'SizeGB'; Expression = { $_.Size / 1gb -as [int] } },
@{Name = 'FreeGB'; Expression = { $_.FreeSpace / 1gb -as [int] } },
@{Name = 'PctFree'; Expression = { '{0:p2}' -f ($_.FreeSpace / $_.Size) } },
Compressed, QuotasDisabled,
@{Name = 'ReportType'; Expression = { 'DiskInfo' } }
}
Write-Host "Starting $($CimSess.count) DiskInfo job(s)" -ForegroundColor Cyan
$cimSess | ForEach-Object {
$Jobs += Start-ThreadJob -ScriptBlock $disk -ArgumentList $_ -Name DiskInfo
}
#processes
$proc = {
param($CimSession)
#get running processes with a working set size greater than 250MB
Get-CimInstance -ClassName win32_process -Filter "WorkingSetSize>=$(250MB)" -CimSession $CimSession |
Select-Object ProcessID, Name, HandleCount, WorkingSetSize,
@{Name = 'Runtime'; Expression = {'{0:dd\.hh\:mm\:ss}' -f (New-TimeSpan -Start $_.CreationDate -End (Get-Date))}},
@{Name = 'ComputerName'; Expression = { $_.CSName } },
@{Name = 'ReportType'; Expression = { 'ProcessInfo' } }
}
Write-Host "Starting $($CimSess.count) Process job(s)" -ForegroundColor Cyan
$cimSess | ForEach-Object {
$Jobs += Start-ThreadJob -ScriptBlock $proc -ArgumentList $_ -Name ProcessInfo
}
#System event log
$sb = {
param(
[string]$ComputerName,
[PSCredential]$Credential
)
#get System log errors within the last 24 hours
$Since = (Get-Date).AddHours(-24).Date
try {
$logData = Get-WinEvent @PSBoundParameters -FilterHashtable @{LogName = 'System'; Level = 2, 3; StartTime = $Since } -ErrorAction Stop
#write a custom object to the pipeline
[PSCustomObject]@{
ComputerName = $ComputerName.toUpper()
LogName = 'System'
LogData = $LogData
Since = $Since
ReportType = 'LogInfo'
}
} #try
catch {
Write-Host "No recent errors or warnings in the System event log on $($ComputerName.ToUpper()) for errors and warnings." -ForegroundColor Yellow
}
}
#Get-WinEvent can't use CIMSessions
Write-Host 'Starting SysLogInfo job(s)' -ForegroundColor Cyan
foreach ($computer in $ComputerName) {
#Dynamically create the argument list
$argList = @($computer)
if ($Credential) {
$argList += $Credential
}
Write-Host "...$computer" -ForegroundColor cyan
$jobs += Start-ThreadJob -ScriptBlock $sb -ArgumentList $argList -Name sysLog
}
Write-Host 'Waiting for all jobs to complete' -ForegroundColor Yellow
#wait for jobs
[void]($jobs | Wait-Job)
Write-Host 'All Jobs complete' -ForegroundColor Green
#create HTML report
Write-Host "Creating HTML report $FilePath" -ForegroundColor Cyan
$fragments = @()
#import style from external file
$style = Get-Content -Path $CSSFile -Raw
$head = @"
<head>
<Title>System Status Report</Title>
<style>
$style
</style>
</head>
"@
$fragments += '<H1>System Status Report</H1>'
#Process jobs in the order I want them processed using the computer name
$data = $jobs | Receive-Job -Keep | Sort-Object -Property ComputerName
$grouped = $data | Group-Object -Property ComputerName
foreach ($item in $grouped) {
$fragments += "<H2>$($item.Name)</H2>"
'OSInfo', 'DiskInfo', 'ProcessInfo', 'LogInfo' | ForEach-Object {
$rType = $_
$grpData = $item.group | Where-Object { $_.ReportType -eq $rType }
switch ($rType) {
'OSInfo' {
$heading = 'Operating System Information'
$as = 'Table'
$content = $grpData | Select-Object -Property * -ExcludeProperty ComputerName, ReportType
}
'DiskInfo' {
$heading = 'Logical Disk Information'
$as = 'Table'
$content = $grpData | Sort-Object DeviceID | Select-Object -Property * -ExcludeProperty ComputerName, ReportType
}
'ProcessInfo' {
$heading = 'Process Information'
$as = 'Table'
if ($grpData.Count -eq 0) {
$content = [PSCustomObject]@{Information = 'No processes with a working set size greater than 250MB found' }
}
else {
$content = $grpData | Sort-Object -Property WorkingSetSize -Descending |
Select-Object ProcessID, Name, HandleCount,
@{Name = 'WorkingSetMB'; Expression = { ($_.WorkingSetSize / 1MB -as [Int]) } },
Runtime
}
}
'LogInfo' {
$heading = 'System Log Information'
$as = 'Table'
if ($grpData.LogData.Count -eq 0) {
$content = [PSCustomObject]@{Information = 'No recent errors or warnings in the System event log.' }
}
else {
#sort by time created
$content = $grpData.LogData | Sort-Object -Property TimeCreated |
Select-Object -Property TimeCreated,
@{Name = 'Source'; Expression = { $_.ProviderName } },
@{Name = 'Type'; Expression = { $_.LevelDisplayName } },
@{Name = 'EventID'; Expression = { $_.Id } }, Message
}
}
} #switch
$fragments += "<H3>$heading</H3>"
$fragments += $Content | ConvertTo-Html -As $As -Fragment
} #foreach report type
} #foreach computer
$fragments += @"
<p class='footer'>
Report run : $((Get-Date).ToUniversalTime()) UTC by $($env:UserDomain)\$($env:UserName)
<br>
Report script : $(Convert-Path $($MyInvocation.InvocationName))
<br>
Report version: $ver
<br>
Report source : $env:ComputerName
</p>
"@
ConvertTo-Html -Body $fragments -Head $head | Out-File -FilePath $FilePath
#cleanup
if ($cimSess) {
Write-Host "Closing $($cimSess.count) CimSession(s)" -ForegroundColor Cyan
$cimSess | Remove-CimSession
}
Write-Host "Removing $($jobs.count) job(s)" -ForegroundColor Cyan
$jobs | Remove-Job
if ($Passthru) {
Get-Item -Path $FilePath
}
else {
Write-Host "Script complete! See $FilePath for details." -ForegroundColor Green
}
#End of Script
The script uses thread jobs to gather system information concurrently. Once the jobs are complete, the results are received and processed using fragments to create a final HTML document.
This is not a production-ready script but it should be good enough to run locally.
Dynamic Styling
But what if you want to add some style at a more granular level? Or be dynamic? Let's say you've generated an HTML fragment like this:
<table>
<colgroup><col/><col/><col/><col/><col/></colgroup>
<tr><th>Id</th><th>Name</th><th>HandleCount</th><th>WorkingSet</th><th>StartTime</th></tr>
<tr><td>8536</td><td>Code</td><td>515</td><td>-2075430912</td><td>7/2/2025 1:49:50 PM</td></tr>
<tr><td>30792</td><td>brave</td><td>537</td><td>678764544</td><td>7/1/2025 9:51:01 AM</td></tr>
<tr><td>26948</td><td>Discord</td><td>1260</td><td>625156096</td><td>7/1/2025 9:51:56 AM</td></tr>
<tr><td>39144</td><td>Code</td><td>694</td><td>537006080</td><td>7/2/2025 1:49:49 PM</td></tr>
<tr><td>4616</td><td>MuseScore4</td><td>5195</td><td>535429120</td><td>7/2/2025 2:38:29 PM</td></tr>
<tr><td>9272</td><td>pwsh</td><td>1595</td><td>533483520</td><td>7/2/2025 1:52:26 PM</td></tr>
<tr><td>17444</td><td>thunderbird</td><td>1413</td><td>523317248</td><td>7/1/2025 9:50:57 AM</td></tr>
<tr><td>16808</td><td>Dropbox</td><td>7922</td><td>503992320</td><td>7/1/2025 9:47:14 AM</td></tr>
<tr><td>4920</td><td>Memory Compression</td><td>0</td><td>479318016</td><td>7/1/2025 9:45:34 AM</td></tr>
<tr><td>7392</td><td>pwsh</td><td>1876</td><td>420990976</td><td>7/1/2025 3:27:02 PM</td></tr>
</table>
You want to highlight WorkingSet
values greater than 500MB. By now, it should be clear I need to add a class in the CSS file.
.warning {
background-color: darkorange !important;
color: white !important;
font-weight: 700 !important;
}
I am using the !important
directive to force the style. Other parts of the CSS file might style the same element and I want to ensure that my warning
class takes precedence. This should be an exception more than the rule.
But how can I dynamically apply this class to the HTML fragment? The trick that I've always used, is to temporarily treat the fragment as an XML document.
[xml]$tmp = $content | ConvertTo-Html -As $as -Fragment
The next step assumes you what the content looks like. I need to loop through the table rows starting at row 2. Remember that everything will be indexed starting at 0. The first row is the header row, so I will skip it.
I then need to get the value of the WorkingSet
column, which is the 4th column in the table. The td
collection is zero-based, so I will use index 3 for the <td>
element.
#skip the header row and start at 1
for ($i = 1; $i -le $tmp.table.tr.count - 1; $i++) {
[double]$ws = $tmp.table.tr[$i].td[3]
if ( $ws -ge 750) {
#set only the type cell
$tmp.table.tr[$i].ChildNodes[3].SetAttribute('class', 'alert')
}
elseif ($ws -ge 500) {
#set only the type cell
$tmp.table.tr[$i].ChildNodes[3].SetAttribute('class', 'warning')
}
}
I treat the cell value as a double
so I can compare it to a number. If the value is greater than or equal to 750MB, I apply the alert
class. If it is greater than or equal to 500MB, I apply the warning
class.
Finally, I can convert the XML back to HTML.
$fragments += $tmp.InnerXml
I want to do something similar for the event log part of the report. If the event log entry is an error, I want to apply a class to the entire row.
[xml]$tmp = $content | ConvertTo-Html -As $as -Fragment
for ($i = 1; $i -le $tmp.table.tr.count - 1; $i++) {
if ($tmp.table.tr[$i].td[2] -eq 'Error') {
#set only the type cell
# $tmp.table.tr[$i].ChildNodes[2].SetAttribute('class', 'alert')
#set the color attribute on the entire row
$tmp.table.tr[$i].SetAttribute('class', 'alert')
}
}
The next version of the script uses a different CSS file.
body {
background-color: rgb(255, 255, 200);
font-family: Monospace;
font-size: 12pt;
}
td,
th {
border: 0px solid black;
border-collapse: collapse;
white-space: pre;
padding: 10px;
}
th {
color: white;
background-color: black;
}
table,
tr,
td,
th {
margin: 0px;
white-space: pre;
}
tr:nth-child(odd) {
background-color: LightGray
}
td {
white-space: break-spaces;
word-wrap: break-word;
}
table {
margin-left: 25px;
table-layout: auto;
}
h2 {
font-family: Tahoma;
color: blue;
}
.alert {
background-color: crimson !important;
color: white !important;
font-weight: 700 !important;
}
.warning {
background-color: darkorange !important;
color: white !important;
font-weight: 700 !important;
}
.alertForeground {
color: crimson !important;
font-weight: 600 !important;
}
.warningForeground {
color: darkorange !important;
font-weight: 600 !important;
}
.ok {
color: green !important;
font-weight: 600 !important;
}
.footer {
color: rgb(118, 160, 118);
margin-left: 25px;
font-family: Verdana, Tahoma, Arial, sans-serif;
font-style: italic;
}
And here's the script that uses it.
#requires -version 5.1
#requires -modules Microsoft.PowerShell.ThreadJob,CimCmdlets
#SysReport2.ps1
#parameters should be updated with validation and parameter attributes
param(
[string[]]$ComputerName = $env:ComputerName,
[string]$FilePath = 'C:\temp\SysReport.html',
[PSCredential]$Credential,
[string]$CSSFile = "SysReport2.css",
[switch]$Passthru
)
#script version
$ver = '1.5.0'
Clear-Host
Write-Host "Starting $($MyInvocation.MyCommand)" -ForegroundColor Cyan
#initialize an array for all jobs
$Jobs = @()
#remove FilePath from PSBoundParameters
[Void]$PSBoundParameters.Remove('FilePath')
[Void]$PSBoundParameters.Remove('Passthru')
Write-Host "Creating $($ComputerName.Count) CimSession(s)" -ForegroundColor Cyan
$cimSess = New-CimSession @PSBoundParameters
#OS Info
$os = {
param($CimSession)
Get-CimInstance -ClassName Win32_OperatingSystem -CimSession $CimSession |
Select-Object -Property @{Name = 'ComputerName'; Expression = { $_.CSName } },
@{Name = 'OSName'; Expression = { $_.Caption } }, Version, OSArchitecture, LastBootUpTime,
@{Name= 'PctFreeMemory';Expression = {'{0:p2}' -f (($_.FreePhysicalMemory / $_.TotalVisibleMemorySize))}},
@{Name = 'Uptime'; Expression = { New-TimeSpan -Start $_.LastBootUpTime -End (Get-Date) } },
@{Name = 'ReportType'; Expression = { 'OSInfo' } }
}
Write-Host "Starting $($CimSess.count) OSInfo job(s)" -ForegroundColor Cyan
$cimSess | ForEach-Object {
#jobs are created in the script scope
$Jobs += Start-ThreadJob -ScriptBlock $os -ArgumentList $_ -Name OSInfo
}
#Disk space
$disk = {
param($CimSession)
Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -CimSession $CimSession |
Select-Object -Property DeviceID,
@{Name = 'ComputerName'; Expression = { $_.SystemName } },
@{Name = 'SizeGB'; Expression = { $_.Size / 1gb -as [int] } },
@{Name = 'FreeGB'; Expression = { $_.FreeSpace / 1gb -as [int] } },
@{Name = 'PctFree'; Expression = { '{0:p2}' -f ($_.FreeSpace / $_.Size) } },
Compressed, QuotasDisabled,
@{Name = 'ReportType'; Expression = { 'DiskInfo' } }
}
Write-Host "Starting $($CimSess.count) DiskInfo job(s)" -ForegroundColor Cyan
$cimSess | ForEach-Object {
$Jobs += Start-ThreadJob -ScriptBlock $disk -ArgumentList $_ -Name DiskInfo
}
#processes
$proc = {
param($CimSession)
#get running processes with a working set size greater than 250MB
Get-CimInstance -ClassName win32_process -Filter "WorkingSetSize>=$(250MB)" -CimSession $CimSession |
Select-Object ProcessID, Name, HandleCount, WorkingSetSize,
@{Name = 'Runtime'; Expression = {'{0:dd\.hh\:mm\:ss}' -f (New-TimeSpan -Start $_.CreationDate -End (Get-Date))}},
@{Name = 'ComputerName'; Expression = { $_.CSName } },
@{Name = 'ReportType'; Expression = { 'ProcessInfo' } }
}
Write-Host "Starting $($CimSess.count) Process job(s)" -ForegroundColor Cyan
$cimSess | ForEach-Object {
$Jobs += Start-ThreadJob -ScriptBlock $proc -ArgumentList $_ -Name ProcessInfo
}
#System event log
$sb = {
param(
[string]$ComputerName,
[PSCredential]$Credential
)
#get System log errors within the last 24 hours
$Since = (Get-Date).AddHours(-24).Date
# write-Host "Querying System event log on $ComputerName" -ForegroundColor magenta
try {
$logData = Get-WinEvent @PSBoundParameters -FilterHashtable @{LogName = 'System'; Level = 2, 3; StartTime = $Since } -ErrorAction Stop
#write a custom object to the pipeline
[PSCustomObject]@{
ComputerName = $ComputerName.toUpper()
LogName = 'System'
LogData = $LogData
Since = $Since
ReportType = 'LogInfo'
}
} #try
catch {
Write-Host "No recent errors or warnings in the System event log on $($ComputerName.ToUpper()) for errors and warnings." -ForegroundColor Yellow
}
}
Write-Host 'Starting SysLogInfo job(s)' -ForegroundColor Cyan
foreach ($computer in $ComputerName) {
#Dynamically create the argument list
$argList = @($Computer)
if ($Credential) {
$argList += $Credential
}
Write-Host "...$computer" -ForegroundColor cyan
$jobs += Start-ThreadJob -ScriptBlock $sb -ArgumentList $argList -Name sysLog
}
Write-Host 'Waiting for all jobs to complete' -ForegroundColor Yellow
#wait for jobs
[void]($jobs | Wait-Job)
Write-Host 'All Jobs complete' -ForegroundColor Green
#create HTML report
Write-Host "Creating HTML report $FilePath" -ForegroundColor Cyan
$fragments = @()
#import style from external file
$style = Get-Content -Path $CSSFile -Raw
$head = @"
<head>
<Title>System Status Report</Title>
<style>
$style
</style>
</head>
"@
$fragments += '<H1>System Status Report</H1>'
#Process jobs in the order I want them processed
$data = $jobs | Receive-Job -Keep | Sort-Object -Property ComputerName
$grouped = $data | Group-Object -Property ComputerName
foreach ($item in $Grouped) {
$fragments += "<H2>$($item.Name)</H2>"
'OSInfo', 'DiskInfo', 'ProcessInfo', 'LogInfo' | ForEach-Object {
$rType = $_
$grpData = $item.group | Where-Object { $_.ReportType -eq $rType }
switch ($rType) {
'OSInfo' {
$heading = 'Operating System Information'
$as = 'Table'
$content = $grpData | Select-Object -Property * -ExcludeProperty ComputerName, ReportType
[xml]$tmp = $content | ConvertTo-Html -As $as -Fragment
for ($i = 1; $i -le $tmp.table.tr.count - 1; $i++) {
[double]$pct = $tmp.table.tr[$i].td[4].replace('%', '')
if ( $pct -le 20) {
#set only the type cell
$tmp.table.tr[$i].ChildNodes[4].SetAttribute('class', 'alertForeground')
}
elseif ($pct -le 50) {
#set only the type cell
$tmp.table.tr[$i].ChildNodes[4].SetAttribute('class', 'warningForeground')
}
else {
$tmp.table.tr[$i].ChildNodes[4].SetAttribute('class', 'ok')
}
}
$fragments += "<H3>$heading</H3>"
$fragments += $tmp.InnerXml
}
'DiskInfo' {
$heading = 'Logical Disk Information'
$as = 'Table'
$content = $grpData | Sort-Object DeviceID | Select-Object -Property * -ExcludeProperty ComputerName, ReportType
[xml]$tmp = $content | ConvertTo-Html -As $as -Fragment
for ($i = 1; $i -le $tmp.table.tr.count - 1; $i++) {
[double]$pct = $tmp.table.tr[$i].td[3].replace('%', '')
if ( $pct -le 20) {
#set only the type cell
$tmp.table.tr[$i].ChildNodes[3].SetAttribute('class', 'alert')
}
elseif ($pct -le 30) {
#set only the type cell
$tmp.table.tr[$i].ChildNodes[3].SetAttribute('class', 'warning')
}
}
$fragments += "<H3>$heading</H3>"
$fragments += $tmp.InnerXml
}
'ProcessInfo' {
$heading = 'Process Information'
$as = 'Table'
if ($grpData.Count -eq 0) {
$content = [PSCustomObject]@{Information = 'No processes with a working set size greater than 250MB found' }
}
else {
$content = $grpData | Sort-Object -Property WorkingSetSize -Descending |
Select-Object ProcessID, Name, HandleCount,
@{Name = 'WorkingSetMB'; Expression = { ($_.WorkingSetSize / 1MB -as [Int]) } },
Runtime
}
[xml]$tmp = $content | ConvertTo-Html -As $as -Fragment
for ($i = 1; $i -le $tmp.table.tr.count - 1; $i++) {
[double]$ws = $tmp.table.tr[$i].td[3]
if ( $ws -ge 750) {
#set only the type cell
$tmp.table.tr[$i].ChildNodes[3].SetAttribute('class', 'alert')
}
elseif ($ws -ge 500) {
#set only the type cell
$tmp.table.tr[$i].ChildNodes[3].SetAttribute('class', 'warning')
}
}
$fragments += "<H3>$heading</H3>"
$fragments += $tmp.InnerXml
}
'LogInfo' {
$heading = 'System Log Information'
$as = 'Table'
if ($grpData.LogData.Count -eq 0) {
$content = [PSCustomObject]@{Information = 'No recent errors or warnings in the System event log.' }
}
else {
#sort by time created
#create content
$content = $grpData.LogData | Sort-Object -Property TimeCreated |
Select-Object -Property TimeCreated,
@{Name = 'Source'; Expression = { $_.ProviderName } },
@{Name = 'Type'; Expression = { $_.LevelDisplayName } },
@{Name = 'EventID'; Expression = { $_.Id } }, Message
}
#insert a style class for errors
[xml]$tmp = $content | ConvertTo-Html -As $as -Fragment
for ($i = 1; $i -le $tmp.table.tr.count - 1; $i++) {
if ($tmp.table.tr[$i].td[2] -eq 'Error') {
#set only the type cell
# $tmp.table.tr[$i].ChildNodes[2].SetAttribute('class', 'alert')
#set the color attribute on the entire row
$tmp.table.tr[$i].SetAttribute('class', 'alert')
}
}
$fragments += "<H3>$heading</H3>"
$fragments += $tmp.InnerXml
}
} #switch rType
} #foreach report type
} #foreach computer
$fragments += @"
<p class='footer'>
Report run : $((Get-Date).ToUniversalTime()) UTC by $($env:UserDomain)\$($env:UserName)
<br>
Report script : $(Convert-Path $($MyInvocation.InvocationName))
<br>
Report version: $ver
<br>
Report source : $env:ComputerName
</p>
"@
ConvertTo-Html -Body $fragments -Head $head | Out-File -FilePath $FilePath
#cleanup
if ($cimSess) {
Write-Host "Closing $($cimSess.count) CimSession(s)" -ForegroundColor Cyan
$cimSess | Remove-CimSession
}
Write-Host "Removing $($jobs.count) job(s)" -ForegroundColor Cyan
$jobs | Remove-Job
if ($Passthru) {
Get-Item -Path $FilePath
}
else {
Write-Host "Script complete! See $FilePath for details." -ForegroundColor Green
}
#End of Script
I've added dynamic styling to a few other areas of the report. I'll let you review the code to see how I did it.
Here are some screenshots of the final report.




Summary
I think that is more than enough for one day. I hope you are able to spend a little time with my examples. Try them out. Play with the CSS files. Leave questions or comments in the web archive.
Next time, I'll try to wrap this up with some HTML reporting alternatives.