If you have a domain, and that domain is active directory, then it goes without saying you need to keep an eye on these servers - they are absolutely critical to authentication and any domain based operations.
New Reporting (website based)
This is the theory I have come up with and it works very well, it can also be run throughout the day and the website will automatically update, lets start with the "System Overview" this will give you a health style card of the status of all Domain Controllers as below:
If you then scroll down you will see each DC dividend up into its individual sites
Then the buttons on the top will cycle though all the checks that are done, the first is the "System Health" from the "dcdiag report" as below, this tab will show you a green dot for the health of the server as well:
Then the "system log" tab will cover all the system messages which will come up in the test as "syslog" notice this DC is green:
This shows a DC that does have a couple errors in the syslog which is indicated by a red dot, then you can see the error in that tab to diagnose them:
This will now be divided into sections, one for the main report and one for the system information report, you can combine them if required but there are more informative individually.
Current Reporting
Currently, we have a script that runs from the Microsoft Gallery that monitors the domain controllers, while the script is helpful, it gives you lots of information that you have to process and when everything is healthy you see an email with lots of green boxes, you will therefore very quickly at a glance look for green and then move on with the rest of your day.
This check also covers various performance metrics that may not be required in the domain controller health, an example of this would be you have a problem with one of your domain controllers but your daily report in the morning, told you everything was green - however, if you run the same report throughout the day then people will get complacent with the email full of green boxes.
Possibly a change in reporting?
I was then asking myself there must be a better way to do this and give meaningful results, well, yes, but some dynamics need to change.
Stop email Notification
Drop the email reporting, this is actually a very ineffective way of diagnosing a problem, yes, you can get an email with green boxes on it running for example 7 AM in the morning, from my point of view, all this does is complete a tick box exercise of “did we check the domain controllers in the morning?”
Quality and Visibility of the data
The next thing we need to address is the depth of the quality of the data, everything on the previous report was in a box with the status that was Red, Amber or Green - the prerequisite is you need to know what each individual box is telling you - if that box is Amber or red, what impact does that mean for your company?
Data gathering process
If you’re looking at Domain Controller health then the recommended Microsoft checks are to use an application called “dcdiag”
You can remotely on servers if you use the FQDN with the command:
dcdiag /s:<server_fqdn>
This means we can remotely run that particular command on all of the domain controllers present in our domain, we can then take that file and with the HTML generation parse it to report on different health statuses.
New Reporting (website based)
This is the theory I have come up with and it works very well, it can also be run throughout the day and the website will automatically update, lets start with the "System Overview" this will give you a health style card of the status of all Domain Controllers as below:
If you then scroll down you will see each DC dividend up into its individual sites
Then the buttons on the top will cycle though all the checks that are done, the first is the "System Health" from the "dcdiag report" as below, this tab will show you a green dot for the health of the server as well:
Then the "system log" tab will cover all the system messages which will come up in the test as "syslog" notice this DC is green:
This shows a DC that does have a couple errors in the syslog which is indicated by a red dot, then you can see the error in that tab to diagnose them:
This will now be divided into sections, one for the main report and one for the system information report, you can combine them if required but there are more informative individually.
Collect the Data (Main Report)
First we need to collect the data from the Domain Controllers, this is covered by this script and when complete it will create a folder from the location where the script is run, this folder will be called DCResults:
Inside this folder you will see the various diagnostics from all your Domain Controllers:
Script : CollectData.ps1
Inside this folder you will see the various diagnostics from all your Domain Controllers:
Script : CollectData.ps1
[CmdletBinding()]
param(
[Parameter(Mandatory=$false)]
[System.Management.Automation.PSCredential]
[System.Management.Automation.Credential()]
$Credential = [System.Management.Automation.PSCredential]::Empty
)
# Enable verbose output
$VerbosePreference = "Continue"
# Check for Administrator privileges
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$isAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
Write-Error "This script requires Administrator privileges. Please run as Administrator."
exit 1
}
# Get all Domain Controllers from Active Directory
Write-Verbose "Discovering Domain Controllers in the forest..."
try {
$DomainControllers = (Get-ADForest).Domains | ForEach-Object {
Get-ADDomainController -Filter * -Server $_ | Select-Object -ExpandProperty HostName
}
Write-Verbose "Found $($DomainControllers.Count) Domain Controllers:"
$DomainControllers | ForEach-Object { Write-Verbose " - $_" }
} catch {
Write-Error "Failed to discover Domain Controllers: $($_.Exception.Message)"
exit 1
}
# Get the directory where the script is being executed from
$scriptPath = $PSScriptRoot
if (!$scriptPath) {
$scriptPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
}
# Create output directory structure relative to script location - simplified
$systemHealthPath = Join-Path $scriptPath "SystemHealth"
# Create directory if it doesn't exist
if (!(Test-Path -Path $systemHealthPath)) {
Write-Verbose "Creating directory: $systemHealthPath"
New-Item -ItemType Directory -Path $systemHealthPath -Force | Out-Null
}
# Later in the script, the $outputFile path creation becomes:
$outputFile = Join-Path $systemHealthPath "DC_Health_Status_${dc}_$timestamp.txt"
function Test-WMIConnection {
param(
[string]$ComputerName,
[System.Management.Automation.PSCredential]$Credential
)
try {
if ($Credential) {
$wmiTest = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $ComputerName -Credential $Credential -ErrorAction Stop
} else {
$wmiTest = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $ComputerName -ErrorAction Stop
}
return $true
} catch {
Write-Verbose "WMI connection test failed for $ComputerName : $($_.Exception.Message)"
return $false
}
}
function Get-RemoteData {
param(
[string]$ComputerName,
[string]$WMIClass,
[System.Management.Automation.PSCredential]$Credential,
[string]$Filter
)
try {
$wmiParams = @{
ComputerName = $ComputerName
Class = $WMIClass
ErrorAction = 'Stop'
}
if ($Credential) {
$wmiParams['Credential'] = $Credential
}
if ($Filter) {
$wmiParams['Filter'] = $Filter
}
return Get-WmiObject @wmiParams
} catch {
Write-Verbose "Failed to get $WMIClass from $ComputerName : $($_.Exception.Message)"
return $null
}
}
function Get-DCHealth {
[CmdletBinding()]
param (
[string]$DomainController,
[System.Management.Automation.PSCredential]$Credential
)
Write-Verbose "Starting health check for $DomainController..."
$report = @()
$report += "Domain Controller Health Report for $DomainController - Generated on $(Get-Date)`n"
$report += "=" * 80 + "`n"
# Test WMI connectivity first
if (-not (Test-WMIConnection -ComputerName $DomainController -Credential $Credential)) {
$report += "ERROR: Cannot establish WMI connection to $DomainController"
return $report
}
try {
# Get OS Information
Write-Verbose "Getting operating system information..."
$osInfo = Get-RemoteData -ComputerName $DomainController -WMIClass "Win32_OperatingSystem" -Credential $Credential
if ($osInfo) {
$lastBoot = $osInfo.ConvertToDateTime($osInfo.LastBootUpTime)
$uptime = (Get-Date) - $lastBoot
$report += "`nLast Reboot Time:"
$report += "---------------"
$report += "Last Boot: $lastBoot"
$report += "Current Uptime: $($uptime.Days) days, $($uptime.Hours) hours, $($uptime.Minutes) minutes`n"
} else {
$report += "Unable to get system boot time information"
}
# Get patches
Write-Verbose "Getting patch information..."
$patches = Get-RemoteData -ComputerName $DomainController -WMIClass "Win32_QuickFixEngineering" -Credential $Credential |
Sort-Object -Property InstalledOn -Descending | Select-Object -First 5
$report += "`nLast Installed Patches:"
$report += "--------------------"
if ($patches) {
foreach ($patch in $patches) {
$report += "KB{0} - Installed: {1}" -f $patch.HotFixID, $patch.InstalledOn
}
# Calculate patch-to-reboot difference if we have both pieces of information
if ($lastBoot -and $patches[0].InstalledOn) {
$lastPatchDate = $patches[0].InstalledOn
$daysSinceReboot = ($lastPatchDate - $lastBoot).Days
$report += "`nPatch-to-Reboot Compliance:"
$report += "-------------------------"
$report += "Days between last patch and reboot: {0}" -f $daysSinceReboot
if ($daysSinceReboot -gt 7) {
$report += "WARNING: More than 7 days between last patch and reboot!`n"
}
}
} else {
$report += "Unable to retrieve patch information"
}
# Check services
Write-Verbose "Checking AD services..."
$report += "`nActive Directory Services Status:"
$report += "------------------------------"
$services = @("NTDS", "NetLogon", "DNS", "KDC", "DFS", "ADWS")
foreach ($serviceName in $services) {
$svc = Get-RemoteData -ComputerName $DomainController -WMIClass "Win32_Service" -Credential $Credential -Filter "Name='$serviceName'"
if ($svc) {
$report += "{0}: {1}" -f $svc.DisplayName, $svc.State
} else {
$report += "{0}: Not Found" -f $serviceName
}
}
# Get disk space information
Write-Verbose "Getting disk space information..."
$report += "`nDisk Space Information:"
$report += "---------------------"
$disks = Get-RemoteData -ComputerName $DomainController -WMIClass "Win32_LogicalDisk" -Credential $Credential -Filter "DriveType=3"
if ($disks) {
foreach ($disk in $disks) {
$freeSpaceGB = [math]::Round($disk.FreeSpace/1GB, 2)
$totalSpaceGB = [math]::Round($disk.Size/1GB, 2)
$freeSpacePercent = [math]::Round(($disk.FreeSpace/$disk.Size)*100, 2)
$report += "`nDrive {0}:" -f $disk.DeviceID
$report += "Total Space: {0} GB" -f $totalSpaceGB
$report += "Free Space: {0} GB" -f $freeSpaceGB
$report += "Free Space Percentage: {0}%" -f $freeSpacePercent
if ($freeSpacePercent -lt 20) {
$report += "WARNING: Free space is below 20%!"
}
}
} else {
$report += "Unable to retrieve disk space information"
}
# Check replication
Write-Verbose "Checking replication status..."
$report += "`nReplication Status:"
try {
$replStatus = repadmin /showrepl $DomainController /csv | ConvertFrom-Csv
$failedReplications = $replStatus | Where-Object { $_."Number of Failures" -gt 0 }
if ($failedReplications) {
$report += "WARNING: Found failed replications!"
foreach ($fail in $failedReplications) {
$report += "Source DC: {0} - Destination DC: {1}" -f $fail.'Source DC', $fail.'Destination DC'
}
} else {
$report += "Replication status: Healthy"
}
} catch {
$report += "Unable to check replication status: $($_.Exception.Message)"
}
} catch {
$errorMessage = "Error processing {0}: {1}" -f $DomainController, $_.Exception.Message
Write-Error $errorMessage
$report += "`nError: $errorMessage"
}
return $report
}
# Process each Domain Controller
$totalDCs = $DomainControllers.Count
$currentDC = 0
foreach ($dc in $DomainControllers) {
$currentDC++
Write-Verbose "`nProcessing DC $currentDC of $totalDCs : $dc"
$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm"
$outputFile = Join-Path $systemHealthPath "DC_Health_Status_${dc}_$timestamp.txt"
try {
$report = Get-DCHealth -DomainController $dc -Credential $Credential -Verbose
$report | Out-File -FilePath $outputFile -Encoding UTF8
Write-Verbose "Health check completed for $dc. Report saved to: $outputFile"
} catch {
Write-Error "Failed to process $dc : $($_.Exception.Message)"
}
}
Write-Verbose "`nHealth check process completed. Reports saved in: $systemHealthPath"
Create the HTML
When the previous script completes, you then need the HTML generated for this report, when this script is run it will create a folder called "DCDiagReport" as below:
This folder will then contain the HTML required for the graphical reports:
Script : OverviewReporting.ps1
When the previous script completes, you then need the HTML generated for this report, when this script is run it will create a folder called "DCDiagReport" as below:
This folder will then contain the HTML required for the graphical reports:
Script : OverviewReporting.ps1
# Create a directory for the report if it doesn't exist
$reportPath = ".\DCDiagReport"
if (!(Test-Path -Path $reportPath)) {
New-Item -ItemType Directory -Path $reportPath | Out-Null
}
# Define DC to Site mapping
$dcSiteMapping = @{
'clawsome.bear.local' = 'Azure'
'fluffo.bear.local' = 'Azure'
'fuzzykins.bear.local' = 'AWS'
'snuffles.bear.local' = 'AWS'
'grizzly.bear.local' = 'Google Cloud'
'growlie.bear.local' = 'Google Cloud'
'grumbles.bear.local' = 'Cave'
'honeybun.bear.local' = 'Cave'
'honeypot.bear.local' = 'Cave'
}
# Create server SVG template
$serverSvgTemplate = @"
<svg viewBox="0 0 200 280" xmlns="http://www.w3.org/2000/svg">
<!-- Server Chassis -->
<rect x="40" y="40" width="120" height="200" fill="#424242" rx="10"/>
<!-- Server Front Panel -->
<rect x="45" y="50" width="110" height="180" fill="#616161" rx="5"/>
<!-- Status LED -->
<circle cx="160" cy="65" r="5" class="status-led"/>
<!-- Drive Bays -->
<rect x="55" y="80" width="90" height="20" fill="#424242" rx="2"/>
<rect x="55" y="110" width="90" height="20" fill="#424242" rx="2"/>
<rect x="55" y="140" width="90" height="20" fill="#424242" rx="2"/>
<!-- Ventilation Slots -->
<rect x="55" y="170" width="90" height="40" fill="#383838" rx="2"/>
<rect x="60" y="175" width="80" height="5" fill="#424242"/>
<rect x="60" y="185" width="80" height="5" fill="#424242"/>
<rect x="60" y="195" width="80" height="5" fill="#424242"/>
<!-- Power Button -->
<circle cx="65" cy="65" r="5" fill="#666"/>
</svg>
"@
# Save the SVG template
$serverSvgTemplate | Out-File "$reportPath\server-template.svg" -Encoding UTF8
# Function to get site for DC
function Get-DCSite {
param (
[string]$DCName
)
$dcLower = $DCName.ToLower()
if ($dcSiteMapping.ContainsKey($dcLower)) {
return $dcSiteMapping[$dcLower]
}
return "Unknown Site"
}
# Function to create button HTML
function Get-NavigationButtonHtml {
param (
[string]$ReportType,
[string]$CurrentPage
)
$activeClass = if ($ReportType -eq $CurrentPage) { " active" } else { "" }
$buttonText = switch ($ReportType) {
"DCDiag" { "System Health" }
"SystemLog" { "System Logs" }
"Overview" { "Overview" }
}
return @"
<a href="$($ReportType)Report.html" class="nav-button$activeClass">
$buttonText
</a>
"@
}
# Function to parse DCDiag results
function Parse-DCDiagFile {
param (
[string]$FilePath
)
$testResults = @()
$content = Get-Content -Path $FilePath -Raw
$currentTest = $null
$testDetails = @{}
$errorContent = ""
foreach ($line in ($content -split "`r`n")) {
if ($line -match "Starting test: (.+)") {
if ($currentTest -and $currentTest -ne "SystemLog") {
$testResults += [PSCustomObject]@{
TestName = $currentTest
Status = if ($errorContent) { "Failed" } else { "Passed" }
Details = if ($errorContent) { $errorContent } else { "" }
}
}
$currentTest = $matches[1].Trim()
$errorContent = ""
}
elseif ($line -match "\.+ (\w+) (passed|failed) test (.+)") {
if ($currentTest -and $currentTest -ne "SystemLog") {
$testResults += [PSCustomObject]@{
TestName = $currentTest
Status = if ($matches[2] -eq "passed") { "Passed" } else { "Failed" }
Details = if ($errorContent) { $errorContent } else { "" }
}
$currentTest = $null
$errorContent = ""
}
}
elseif ($currentTest -and $currentTest -ne "SystemLog" -and ($line -match "An error event occurred|A warning event occurred|Error:|Warning:")) {
$errorContent += "$line`n"
}
elseif ($currentTest -and $currentTest -ne "SystemLog" -and $line.Trim() -and $errorContent) {
$errorContent += "$line`n"
}
}
return $testResults
}
# Function to parse SystemLog results
function Parse-SystemLogFile {
param (
[string]$FilePath
)
$systemLogContent = ""
$inSystemLog = $false
$content = Get-Content -Path $FilePath -Raw
$status = "Passed"
foreach ($line in ($content -split "`r`n")) {
if ($line -match "Starting test: SystemLog") {
$inSystemLog = $true
}
elseif ($inSystemLog -and $line -match "Starting test:") {
$inSystemLog = $false
}
elseif ($inSystemLog) {
if ($line -match "failed test SystemLog") {
$status = "Failed"
}
if ($line.Trim() -ne "") {
$systemLogContent += "$line`n"
}
}
}
return @{
Status = $status
Details = $systemLogContent
}
}
# Function to determine overall DC status
function Get-DCStatus {
param (
$TestResults
)
foreach ($result in $TestResults) {
if ($result.Status -eq "Failed") {
return "failed"
}
}
return "passed"
}
# Generate and save reports
foreach ($reportType in @("Overview", "DCDiag", "SystemLog")) {
$html = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Domain Controller $(if ($reportType -eq "DCDiag") { "Health" } elseif ($reportType -eq "Overview") { "Overview" } else { "System Log" }) Report</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
background: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
text-align: center;
margin-bottom: 30px;
}
.nav-buttons {
display: flex;
justify-content: center;
gap: 20px;
margin: 20px 0;
}
.nav-button {
padding: 10px 20px;
border: 2px solid #2980b9;
border-radius: 4px;
text-decoration: none;
color: #2980b9;
font-weight: bold;
transition: all 0.3s ease;
}
.nav-button:hover {
background-color: #2980b9;
color: white;
}
.nav-button.active {
background-color: #2980b9;
color: white;
}
.header h1 {
color: #2c3e50;
margin-bottom: 10px;
}
.header .timestamp {
color: #7f8c8d;
font-size: 0.9em;
}
.tabs {
display: flex;
list-style: none;
padding: 0;
margin: 0 0 20px 0;
border-bottom: 2px solid #e0e0e0;
flex-wrap: wrap;
}
.tab-button {
padding: 10px 20px;
border: none;
background: none;
cursor: pointer;
font-size: 1em;
color: #666;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
text-align: center;
min-width: 150px;
}
.tab-button.active {
color: #2980b9;
border-bottom: 2px solid #2980b9;
}
.dc-name {
font-weight: bold;
margin-bottom: 5px;
}
.dc-site {
font-size: 0.8em;
color: #666;
}
.status-dot {
height: 10px;
width: 10px;
border-radius: 50%;
display: inline-block;
margin: 5px auto;
}
.status-dot.passed {
background-color: #4caf50;
}
.status-dot.failed {
background-color: #f44336;
}
.tab-content {
display: none;
padding: 20px;
background: #fff;
border-radius: 0 0 4px 4px;
}
.tab-content.active {
display: block;
}
.test-result {
margin-bottom: 15px;
padding: 10px;
border-radius: 4px;
}
.test-result.passed {
background-color: #e8f5e9;
border-left: 4px solid #4caf50;
}
.test-result.failed {
background-color: #ffebee;
border-left: 4px solid #f44336;
}
.test-name {
font-weight: bold;
margin-bottom: 5px;
}
.error-details {
margin-top: 10px;
padding: 10px;
background: #fff3e0;
border-radius: 4px;
font-size: 0.9em;
white-space: pre-wrap;
font-family: monospace;
}
.log-content {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
font-family: monospace;
white-space: pre-wrap;
font-size: 0.9em;
overflow-x: auto;
}
.overview-grid {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
padding: 20px;
}
.site-group {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.site-title {
font-size: 1.2em;
font-weight: bold;
color: #2c3e50;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #e0e0e0;
}
.servers-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.server-card {
background: white;
border-radius: 8px;
padding: 15px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
}
.server-card:hover {
transform: translateY(-5px);
}
.server-svg {
width: 100px;
height: 140px;
margin: 0 auto;
}
.server-svg .status-led.passed {
fill: #4caf50;
}
.server-svg .status-led.failed {
fill: #f44336;
}
.server-info {
margin-top: 15px;
}
.server-name {
font-weight: bold;
font-size: 1.1em;
margin-bottom: 5px;
}
.server-site {
color: #666;
font-size: 0.9em;
}
.health-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.summary-card {
padding: 20px;
border-radius: 8px;
text-align: center;
color: white;
}
.summary-card.healthy {
background: linear-gradient(135deg, #43a047 0%, #2e7d32 100%);
}
.summary-card.unhealthy {
background: linear-gradient(135deg, #e53935 0%, #c62828 100%);
}
.summary-card.total {
background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%);
}
.summary-number {
font-size: 2.5em;
font-weight: bold;
margin: 10px 0;
}
.summary-label {
font-size: 1.1em;
opacity: 0.9;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Domain Controller Report</h1>
<div class="nav-buttons">
$(Get-NavigationButtonHtml -ReportType "Overview" -CurrentPage $reportType)
$(Get-NavigationButtonHtml -ReportType "DCDiag" -CurrentPage $reportType)
$(Get-NavigationButtonHtml -ReportType "SystemLog" -CurrentPage $reportType)
</div>
<div class="timestamp">Generated: $(Get-Date)</div>
</div>
"@
# Get all DCDiag files
$dcDiagFiles = Get-ChildItem -Path ".\DCResults" -Filter "*_DCDiag_*.txt"
if ($reportType -eq "Overview") {
# Calculate health statistics
$totalServers = $dcDiagFiles.Count
$healthyServers = 0
$unhealthyServers = 0
foreach ($file in $dcDiagFiles) {
$results = Parse-DCDiagFile -FilePath $file.FullName
$status = Get-DCStatus -TestResults $results
if ($status -eq "passed") {
$healthyServers++
} else {
$unhealthyServers++
}
}
$html += @"
<div class="health-summary">
<div class="summary-card total">
<div class="summary-number">$totalServers</div>
<div class="summary-label">Total Servers</div>
</div>
<div class="summary-card healthy">
<div class="summary-number">$healthyServers</div>
<div class="summary-label">Healthy Servers</div>
</div>
<div class="summary-card unhealthy">
<div class="summary-number">$unhealthyServers</div>
<div class="summary-label">Unhealthy Servers</div>
</div>
</div>
<div class="overview-grid">
"@
# Get unique sites from mapping
$sites = $dcSiteMapping.Values | Select-Object -Unique | Sort-Object
foreach ($site in $sites) {
$html += @"
<div class="site-group">
<div class="site-title">$site</div>
<div class="servers-grid">
"@
# Get servers for this site
$siteServers = $dcDiagFiles | Where-Object {
$dcName = $_.BaseName -replace '_DCDiag_\d+$'
$serverSite = Get-DCSite -DCName $dcName
$serverSite -eq $site
}
foreach ($file in $siteServers) {
$dcName = $file.BaseName -replace '_DCDiag_\d+$'
$results = Parse-DCDiagFile -FilePath $file.FullName
$status = Get-DCStatus -TestResults $results
$serverSvg = Get-Content "$reportPath\server-template.svg" -Raw
$serverSvg = $serverSvg -replace 'class="status-led"', "class=`"status-led $status`""
$html += @"
<div class="server-card">
<div class="server-svg">
$serverSvg
</div>
<div class="server-info">
<div class="server-name">$dcName</div>
<div class="status-dot $status"></div>
</div>
</div>
"@
}
$html += @"
</div>
</div>
"@
}
$html += "</div>"
}
else {
$html += @"
<div class="tabs">
"@
# Add tab buttons
$firstDC = $true
foreach ($file in $dcDiagFiles) {
$dcName = $file.BaseName -replace '_DCDiag_\d+$'
if ($reportType -eq "DCDiag") {
$results = Parse-DCDiagFile -FilePath $file.FullName
$status = Get-DCStatus -TestResults $results
} else {
$results = Parse-SystemLogFile -FilePath $file.FullName
$status = $results.Status.ToLower()
}
$site = Get-DCSite -DCName $dcName
$activeClass = if ($firstDC) { ' active' } else { '' }
$html += @"
<button class="tab-button$activeClass" onclick="openTab('$dcName')">
<div class="dc-name">$dcName</div>
<div class="dc-site">$site</div>
<div class="status-dot $status"></div>
</button>
"@
$firstDC = $false
}
$html += " </div>`n"
# Add tab content
$firstDC = $true
foreach ($file in $dcDiagFiles) {
$dcName = $file.BaseName -replace '_DCDiag_\d+$'
$activeClass = if ($firstDC) { ' active' } else { '' }
$site = Get-DCSite -DCName $dcName
$html += @"
<div id="$dcName" class="tab-content$activeClass">
<div class="summary">
<h3>$dcName - $site $(if ($reportType -eq "DCDiag") { "Test Results" } else { "System Log" })</h3>
</div>
"@
if ($reportType -eq "DCDiag") {
$results = Parse-DCDiagFile -FilePath $file.FullName
foreach ($result in $results) {
$statusClass = if ($result.Status -eq "Passed") { "passed" } else { "failed" }
$icon = if ($result.Status -eq "Passed") { "✓" } else { "✗" }
$html += @"
<div class="test-result $statusClass">
<div class="test-name">$($result.TestName)</div>
<div class="status">$icon $($result.Status)</div>
"@
if ($result.Details) {
$html += @"
<div class="error-details">
$($result.Details)
</div>
"@
}
$html += " </div>`n"
}
} else {
$results = Parse-SystemLogFile -FilePath $file.FullName
$html += @"
<div class="log-content">$($results.Details)</div>
"@
}
$html += " </div>`n"
$firstDC = $false
}
}
# Add JavaScript and close HTML
$html += @"
</div>
<script>
function openTab(dcName) {
// Hide all tab content
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
// Deactivate all buttons
document.querySelectorAll('.tab-button').forEach(button => {
button.classList.remove('active');
});
// Show the selected tab content
document.getElementById(dcName).classList.add('active');
// Activate the clicked button
event.currentTarget.classList.add('active');
}
</script>
</body>
</html>
"@
# Save the report
$html | Out-File "$reportPath\$($reportType)Report.html" -Encoding UTF8
Write-Host "$reportType Report has been generated at: $reportPath\$($reportType)Report.html" -ForegroundColor Green
}