What I Needed to Solve
The goal was simple: run a scheduled health check from Azure DevOps, validate Windows servers through WinRM, save the result into a reporting database, and show the latest state in a Blazor dashboard.
- Check server reachability and WinRM connectivity.
- Check IIS site and app pool state.
- Run approved validation scripts when needed.
- Keep a history of snapshots instead of only showing the current state.
Why This Was Worth Building
- Manual checks were repetitive and slow.
- Different teams needed one consistent view.
- A snapshot model gave me history without making the pipeline a live monitoring service.
- WinRM gave me a controlled way to execute PowerShell on Windows targets.
How I Structured It
| Layer | Responsibility |
|---|---|
| Azure DevOps pipeline | Schedules checks and runs the validation script |
| PowerShell script | Connects over WinRM, runs the checks, writes JSON |
| Reporting API | Stores snapshots into a lower-environment database |
| Blazor dashboard | Reads the reporting data and renders the operator view |
Demo Dashboard Preview
This small preview shows the shape of the dashboard I built. The real UI reads from the reporting DB, but this captures the summary cards and server grid clearly.
Total Servers
Passed
Warning
Failed
| Server | Environment | Last Check | Status |
|---|---|---|---|
| APP-SRV-01 | QA | 2026-06-05 10:00 UTC | Passed |
| WEB-SRV-02 | UAT | 2026-06-05 10:00 UTC | Warning |
| IIS-PROD-03 | PROD | 2026-06-05 10:00 UTC | Failed |
PowerShell Validation Shape
The script ran server-by-server, returned JSON, and stayed read-only unless I intentionally added a remedial step later.
param(
[string]$ServerListFile,
[string]$OutputDir
)
$servers = Get-Content $ServerListFile | ConvertFrom-Json
foreach ($server in $servers) {
$result = [pscustomobject]@{
server = $server.name
environment = $server.environment
checkedAtUtc = (Get-Date).ToUniversalTime().ToString("o")
checks = @(
[pscustomobject]@{ name = "Ping"; status = "Passed"; message = "Reachable" },
[pscustomobject]@{ name = "IIS"; status = "Passed"; message = "Site started" }
)
}
$result | ConvertTo-Json -Depth 8 | Set-Content (Join-Path $OutputDir "$($server.name).json")
}
Blazor Dashboard Shape
The dashboard reads the latest snapshot, shows the summary cards, and renders each server with its check breakdown.
@page "/health-dashboard"
@inject HttpClient Http
@if (_model is null)
{
Loading...
}
else
{
Server Health Dashboard
Last refresh: @_lastRefreshText
Data Panel
Reference table for quick scanning and comparison.
Reference
Live
Status
Server
Environment
Status
@foreach (var row in _model.Rows)
{
@row.Server
@row.Environment
@row.Status
}
}
Security Controls I Kept
- Used a dedicated WinRM account with least privilege.
- Kept the validation job read-only.
- Stored secrets in secure pipeline variables or Key Vault.
- Separated validation from remediation.
What I Would Show Next
The next useful step is to wire the reporting API and the dashboard component to the snapshot DB so the preview becomes live data.