I manage several domain controllers, and like many administrators, I rely on Windows Server Backup for regular backups. The problem is that checking backup status across multiple servers becomes tedious quickly. Logging into each server, opening Event Viewer, and manually checking for backup events is time-consuming and error-prone. I needed a way to see the backup status of all domain controllers at a glance.
Visuals of Website
This is the resulting website from the live backup data, you can clearly see that one Domain Controller has a backup issue with the error shown below in the this blog post:
Understanding Windows Server Backup Events
Windows Server Backup writes specific events to the Microsoft-Windows-Backup operational log when backups run. There are three event IDs I need to track:
- Event ID 4: Backup completed successfully
- Event ID 5: Backup failed with an error
- Event ID 7: Backup completed but with errors
Event ID 4 is straightforward - it simply states "The backup operation has finished successfully." This is what I want to see.
Event ID 5 indicates a complete failure. The message typically looks like this:
The backup operation that started at '2025-10-07T19:00:06.212053500Z' has failed
with following error code '0x80780102' (The system writer is not found in the backup.).
Please review the event details for a solution, and then rerun the backup operation
once the issue is resolved.
Event ID 7 sits somewhere in between - the backup ran, but something went wrong. The message format is similar to Event ID 5, but the operation technically completed despite the errors.
Querying Domain Controllers
The first script queries all domain controllers in the domain for these three event IDs. I start by retrieving the list of domain controllers:
$DomainControllers = Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName
Then I loop through each domain controller and query the backup log:
$Events = Get-WinEvent -ComputerName $DC -FilterHashtable @{
LogName = 'Microsoft-Windows-Backup'
ID = 4,5,7
} -ErrorAction Stop
Using -FilterHashtable is more efficient than filtering after retrieval, especially when dealing with large event logs. The Get-WinEvent cmdlet connects to each remote server and pulls only the events I need.
For each event, I determine the status based on the event ID:
switch ($Event.Id) {
4 { $Status = 'Success' }
5 { $Status = 'Failed' }
7 { $Status = 'Warning' }
default { $Status = 'Unknown' }
}
Event IDs 5 and 7 contain error codes in their messages, which I extract using a regular expression:
if ($Event.Id -eq 5 -or $Event.Id -eq 7) {
if ($Event.Message -match "'(0x[0-9A-Fa-f]+)'") {
$ErrorCode = $matches[1]
}
}
The script collects all this information and exports it to a CSV file with a timestamp:
$Results | Sort-Object TimeCreated -Descending | Export-Csv -Path $OutputFile -NoTypeInformation
I sort by TimeCreated in descending order so the most recent backups appear first in the CSV.
Generating the HTML Dashboard
The second script reads the CSV file and generates an HTML dashboard. I find the latest CSV file automatically:
$LatestCSV = Get-ChildItem -Path "DC_Backup_Events_*.csv" |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
Then I import the data and convert it to JSON:
$BackupData = Import-Csv -Path $LatestCSV.FullName
$JsonData = $BackupData | ConvertTo-Json -Compress
The JSON data gets embedded directly into the HTML file. This creates a completely self-contained dashboard - no external dependencies, no web server required. Just open the HTML file in a browser.
The Grid Layout
I originally tried a timeline view with dots and dates, but it was difficult to scan quickly. The grid format works much better. Each domain controller gets its own card, and each backup is represented by a colored square:
.servers-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
The auto-fit and minmax combination creates a responsive layout that adjusts based on screen width. On a wide monitor, I can see multiple servers side-by-side. On a laptop, they stack vertically.
Inside each card, the backups are displayed in a 7-column grid:
.backup-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 6px;
}
Seven columns gives a roughly weekly pattern, which makes it easier to spot when backups might have failed on a particular day of the week. Each backup is a square that maintains its aspect ratio:
.backup-dot {
width: 100%;
aspect-ratio: 1;
border-radius: 4px;
}
Color Coding
The color scheme is simple: green for success, red for failure, orange for warnings. I use specific hex values that are easy to distinguish:
.backup-dot.success { background: #27ae60; }
.backup-dot.failure { background: #e74c3c; }
.backup-dot.warning { background: #f39c12; }
Handling the Date Format
PowerShell exports dates in UK format (DD/MM/YYYY), which JavaScript doesn't parse natively. I handle this by splitting the date string and reconstructing it:
const dateParts = item.TimeCreated.split(/[\s\/\:]/);
const parsedDate = new Date(
parseInt(dateParts[2]), // year
parseInt(dateParts[1]) - 1, // month (0-indexed)
parseInt(dateParts[0]), // day
parseInt(dateParts[3] || 0), // hour
parseInt(dateParts[4] || 0), // minute
parseInt(dateParts[5] || 0) // second
);
The regex /[\s\/\:]/ splits on spaces, slashes, and colons, giving me an array of date components. JavaScript months are zero-indexed (January = 0), so I subtract 1 from the month value.
Tooltips on Hover
Each colored square displays a tooltip when you hover over it. I use CSS ::after pseudo-elements for this:
.backup-dot::after {
content: attr(data-date);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(-5px);
background: #2c3e50;
color: white;
padding: 6px 10px;
border-radius: 4px;
opacity: 0;
}
.backup-dot:hover::after {
opacity: 1;
}
The tooltip content comes from the data-date attribute on each element. This approach is cleaner than JavaScript-based tooltips and doesn't require any libraries.
Filtering
The dashboard includes three filters: server, status, and a search box. The status filter is straightforward:
if (statusFilterValue === 'success') {
filteredData = filteredData.filter(item => item.isSuccess);
} else if (statusFilterValue === 'failure') {
filteredData = filteredData.filter(item => item.isFailure);
} else if (statusFilterValue === 'warning') {
filteredData = filteredData.filter(item => item.isWarning);
}
The search box searches across server names, status values, and messages:
if (searchFilterValue) {
filteredData = filteredData.filter(item =>
item.Message.toLowerCase().includes(searchFilterValue) ||
item.Status.toLowerCase().includes(searchFilterValue) ||
item.ServerName.toLowerCase().includes(searchFilterValue)
);
}
Every time a filter changes, the applyFilters() function recalculates the statistics and re-renders the display. This happens instantly since all the data is already loaded in memory.
The Complete Workflow
Running the monitoring system is a two-step process:
Run the query script to collect backup events from all domain controllers:
.\get-backup-report.ps1Generate the HTML dashboard from the CSV data:
.\Generate-Backup-Dashboard.ps1
The dashboard file is timestamped, so I can keep historical snapshots if needed. Each HTML file is completely standalone - I can email it, archive it, or open it on any computer with a web browser.
Conclusion
This solution emerged from a simple frustration with checking multiple servers manually. The combination of PowerShell for data collection and a static HTML dashboard for visualization works well because it requires no infrastructure - no databases, no web servers, no external dependencies.
The grid layout was an iterative improvement. The initial timeline view looked clean but proved impractical when scanning dozens of backup events. The grid format, with its consistent spacing and color coding, makes patterns immediately visible. A streak of red squares stands out and a white gap indicates a server that hasn't been backed up recently