Common/Build-DriftHtml.ps1
|
function Build-DriftHtml { <# .SYNOPSIS Builds the Drift Analysis HTML section for the assessment report. .DESCRIPTION Renders a self-contained report page showing all changes detected between the current assessment and a named baseline. Changes are classified as Regressed, Improved, Modified, New, or Removed and colour-coded. .PARAMETER DriftReport Array of drift result objects from Compare-AssessmentBaseline. .PARAMETER BaselineLabel Human-readable label of the baseline that was compared (e.g. 'Q1-2026'). .PARAMETER BaselineTimestamp ISO 8601 timestamp from the baseline metadata, shown in the section header. .EXAMPLE $driftHtml = Build-DriftHtml -DriftReport $driftReport -BaselineLabel 'Q1-2026' -BaselineTimestamp '2026-01-01T00:00:00Z' #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [PSCustomObject[]]$DriftReport, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$BaselineLabel, [Parameter()] [string]$BaselineTimestamp = '' ) $html = [System.Text.StringBuilder]::new() $regressed = @($DriftReport | Where-Object { $_.ChangeType -eq 'Regressed' }) $improved = @($DriftReport | Where-Object { $_.ChangeType -eq 'Improved' }) $modified = @($DriftReport | Where-Object { $_.ChangeType -eq 'Modified' }) $newChecks = @($DriftReport | Where-Object { $_.ChangeType -eq 'New' }) $removed = @($DriftReport | Where-Object { $_.ChangeType -eq 'Removed' }) $totalChanged = $DriftReport.Count $timestampDisplay = if ($BaselineTimestamp) { try { ([datetime]$BaselineTimestamp).ToString('yyyy-MM-dd HH:mm') } catch { $BaselineTimestamp } } else { '' } $null = $html.AppendLine("<div class='report-page' data-page='drift-analysis' id='drift-analysis'>") $null = $html.AppendLine("<details class='section' id='drift-analysis-section' open>") $null = $html.AppendLine("<summary><h2>Drift Analysis</h2></summary>") # Header: baseline info + stat tiles $null = $html.AppendLine("<div class='drift-header'>") $null = $html.AppendLine("<div class='drift-baseline-info'>") $null = $html.AppendLine("<span class='drift-label-badge'>Baseline: $([System.Net.WebUtility]::HtmlEncode($BaselineLabel))</span>") if ($timestampDisplay) { $null = $html.AppendLine("<span class='drift-timestamp'>Captured: $timestampDisplay</span>") } $null = $html.AppendLine("</div>") # Stat tiles $null = $html.AppendLine("<div class='drift-stats'>") if ($regressed.Count -gt 0) { $null = $html.AppendLine("<div class='drift-stat drift-stat-regressed'><span class='stat-num'>$($regressed.Count)</span><span class='stat-label'>Regressed</span></div>") } if ($improved.Count -gt 0) { $null = $html.AppendLine("<div class='drift-stat drift-stat-improved'><span class='stat-num'>$($improved.Count)</span><span class='stat-label'>Improved</span></div>") } if ($modified.Count -gt 0) { $null = $html.AppendLine("<div class='drift-stat drift-stat-modified'><span class='stat-num'>$($modified.Count)</span><span class='stat-label'>Modified</span></div>") } if ($newChecks.Count -gt 0) { $null = $html.AppendLine("<div class='drift-stat drift-stat-new'><span class='stat-num'>$($newChecks.Count)</span><span class='stat-label'>New</span></div>") } if ($removed.Count -gt 0) { $null = $html.AppendLine("<div class='drift-stat drift-stat-removed'><span class='stat-num'>$($removed.Count)</span><span class='stat-label'>Removed</span></div>") } $null = $html.AppendLine("</div>") # drift-stats $null = $html.AppendLine("</div>") # drift-header if ($totalChanged -eq 0) { $null = $html.AppendLine("<div class='drift-no-changes'><p>No changes detected since baseline <strong>$([System.Net.WebUtility]::HtmlEncode($BaselineLabel))</strong>. All assessed checks match their baseline values.</p></div>") $null = $html.AppendLine("</details></div>") return $html.ToString() } # Sort: Regressed first, then Improved, Modified, New, Removed $sortOrder = @{ Regressed = 0; Improved = 1; Modified = 2; New = 3; Removed = 4 } $sorted = $DriftReport | Sort-Object -Property @{ Expression = { $sortOrder[$_.ChangeType] } }, Section, Setting $null = $html.AppendLine("<details class='collector-detail' open>") $null = $html.AppendLine("<summary><h3>Changed Checks</h3><span class='row-count'>($totalChanged changes)</span></summary>") $null = $html.AppendLine("<div class='table-wrapper'>") $null = $html.AppendLine("<table class='data-table drift-table'>") $null = $html.AppendLine("<thead><tr>") $null = $html.AppendLine("<th>Change</th><th>Section</th><th>Check</th><th>Before</th><th>After</th>") $null = $html.AppendLine("</tr></thead><tbody>") foreach ($row in $sorted) { $changeType = $row.ChangeType $rowClass = switch ($changeType) { 'Regressed' { 'drift-row-regressed' } 'Improved' { 'drift-row-improved' } 'Modified' { 'drift-row-modified' } 'New' { 'drift-row-new' } 'Removed' { 'drift-row-removed' } default { '' } } $chipClass = switch ($changeType) { 'Regressed' { 'drift-chip-regressed' } 'Improved' { 'drift-chip-improved' } 'Modified' { 'drift-chip-modified' } 'New' { 'drift-chip-new' } 'Removed' { 'drift-chip-removed' } default { '' } } $settingEncoded = [System.Net.WebUtility]::HtmlEncode($row.Setting) $sectionEncoded = [System.Net.WebUtility]::HtmlEncode($row.Section) $prevStatusEnc = [System.Net.WebUtility]::HtmlEncode($row.PreviousStatus) $currStatusEnc = [System.Net.WebUtility]::HtmlEncode($row.CurrentStatus) $prevValueEnc = [System.Net.WebUtility]::HtmlEncode($row.PreviousValue) $currValueEnc = [System.Net.WebUtility]::HtmlEncode($row.CurrentValue) # Before / after cell content: show status if changed, else show value $beforeCell = if ($prevStatusEnc) { "<span class='badge badge-neutral'>$prevStatusEnc</span>" } else { '<span class="drift-empty">—</span>' } $afterCell = if ($currStatusEnc) { "<span class='badge badge-neutral'>$currStatusEnc</span>" } else { '<span class="drift-empty">—</span>' } # If status unchanged but value changed, show the value instead if ($row.PreviousStatus -eq $row.CurrentStatus -and ($prevValueEnc -or $currValueEnc)) { $beforeCell = "<span class='drift-value'>$prevValueEnc</span>" $afterCell = "<span class='drift-value'>$currValueEnc</span>" } $null = $html.AppendLine("<tr class='$rowClass'>") $null = $html.AppendLine("<td><span class='drift-chip $chipClass'>$changeType</span></td>") $null = $html.AppendLine("<td>$sectionEncoded</td>") $null = $html.AppendLine("<td>$settingEncoded</td>") $null = $html.AppendLine("<td>$beforeCell</td>") $null = $html.AppendLine("<td>$afterCell</td>") $null = $html.AppendLine("</tr>") } $null = $html.AppendLine("</tbody></table>") $null = $html.AppendLine("</div>") # table-wrapper $null = $html.AppendLine("</details>") # collector-detail $null = $html.AppendLine("</details>") # section $null = $html.AppendLine("</div>") # report-page return $html.ToString() } |