Private/New-HtmlDashboard.ps1
|
function New-HtmlDashboard { <# .SYNOPSIS Generates an HTML dashboard report with inline CSS. .DESCRIPTION Creates styled HTML reports for execution results, health scores, and shift handoff. Uses inline CSS only (no external stylesheets) with the accent color #f97316 (orange-fire). Supports multiple report types: ExecutionReport, HealthScore, ShiftHandoff. #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('ExecutionReport', 'HealthScore', 'ShiftHandoff')] [string]$ReportType, [Parameter(Mandatory)] [object]$Data, [Parameter(Mandatory)] [string]$OutputPath, [Parameter()] [string]$Title ) $accentColor = '#f97316' $accentDark = '#ea580c' $bgColor = '#0f172a' $cardBg = '#1e293b' $textColor = '#e2e8f0' $textMuted = '#94a3b8' $successColor = '#22c55e' $warningColor = '#eab308' $errorColor = '#ef4444' $infoColor = '#3b82f6' $css = @" * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: $bgColor; color: $textColor; padding: 2rem; line-height: 1.6; } .container { max-width: 1200px; margin: 0 auto; } .header { background: linear-gradient(135deg, $accentColor, $accentDark); padding: 2rem; border-radius: 12px; margin-bottom: 2rem; } .header h1 { font-size: 1.8rem; font-weight: 700; color: white; } .header p { color: rgba(255,255,255,0.85); margin-top: 0.5rem; } .card { background: $cardBg; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; border: 1px solid rgba(255,255,255,0.05); } .card h2 { color: $accentColor; font-size: 1.2rem; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid rgba(255,255,255,0.1); } .card h3 { color: $textColor; font-size: 1rem; margin-bottom: 0.75rem; } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; margin-bottom: 1.5rem; } .stat-card { background: $cardBg; border-radius: 12px; padding: 1.5rem; border: 1px solid rgba(255,255,255,0.05); text-align: center; } .stat-value { font-size: 2.5rem; font-weight: 700; color: $accentColor; } .stat-label { color: $textMuted; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; margin-top: 0.25rem; } table { width: 100%; border-collapse: collapse; margin-top: 0.5rem; } th { background: rgba(249,115,22,0.15); color: $accentColor; padding: 0.75rem 1rem; text-align: left; font-weight: 600; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.03em; } td { padding: 0.75rem 1rem; border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 0.9rem; } tr:hover td { background: rgba(255,255,255,0.02); } .badge { display: inline-block; padding: 0.2rem 0.6rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; } .badge-success { background: rgba(34,197,94,0.15); color: $successColor; } .badge-warning { background: rgba(234,179,8,0.15); color: $warningColor; } .badge-error { background: rgba(239,68,68,0.15); color: $errorColor; } .badge-info { background: rgba(59,130,246,0.15); color: $infoColor; } .badge-neutral { background: rgba(148,163,184,0.15); color: $textMuted; } .score-ring { width: 120px; height: 120px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 1rem auto; font-size: 2rem; font-weight: 700; } .grade-a { border: 4px solid $successColor; color: $successColor; } .grade-b { border: 4px solid #22d3ee; color: #22d3ee; } .grade-c { border: 4px solid $warningColor; color: $warningColor; } .grade-d { border: 4px solid $accentColor; color: $accentColor; } .grade-f { border: 4px solid $errorColor; color: $errorColor; } .step-flow { position: relative; padding-left: 2rem; } .step-item { position: relative; padding-bottom: 1.5rem; padding-left: 1.5rem; border-left: 2px solid rgba(255,255,255,0.1); } .step-item:last-child { border-left: 2px solid transparent; } .step-dot { position: absolute; left: -0.5rem; top: 0.2rem; width: 1rem; height: 1rem; border-radius: 50%; } .step-dot.success { background: $successColor; } .step-dot.failure { background: $errorColor; } .step-dot.skipped { background: $textMuted; } .step-dot.pending { background: $warningColor; } .step-title { font-weight: 600; margin-bottom: 0.25rem; } .step-detail { color: $textMuted; font-size: 0.85rem; } .footer { text-align: center; color: $textMuted; font-size: 0.8rem; margin-top: 2rem; padding-top: 1rem; border-top: 1px solid rgba(255,255,255,0.05); } pre { background: rgba(0,0,0,0.3); padding: 1rem; border-radius: 8px; overflow-x: auto; font-size: 0.85rem; color: $textMuted; margin-top: 0.5rem; } "@ $timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') $bodyContent = '' switch ($ReportType) { 'ExecutionReport' { if (-not $Title) { $Title = "Runbook Execution Report" } $statusBadge = switch ($Data.Status) { 'Completed' { '<span class="badge badge-success">Completed</span>' } 'Failed' { '<span class="badge badge-error">Failed</span>' } 'Escalated' { '<span class="badge badge-warning">Escalated</span>' } 'Aborted' { '<span class="badge badge-error">Aborted</span>' } default { '<span class="badge badge-neutral">' + $Data.Status + '</span>' } } $duration = if ($Data.Duration) { $Data.Duration } else { 'N/A' } $bodyContent = @" <div class="header"> <h1>$Title</h1> <p>Runbook: $($Data.RunbookName) | Target: $($Data.ComputerName) | $timestamp</p> </div> <div class="grid"> <div class="stat-card"> <div class="stat-value">$statusBadge</div> <div class="stat-label">Status</div> </div> <div class="stat-card"> <div class="stat-value">$duration</div> <div class="stat-label">Duration</div> </div> <div class="stat-card"> <div class="stat-value">$(if ($Data.StepResults) { @($Data.StepResults).Count } else { 0 })</div> <div class="stat-label">Steps Executed</div> </div> </div> "@ # Step flow if ($Data.StepResults) { $bodyContent += '<div class="card"><h2>Execution Flow</h2><div class="step-flow">' foreach ($step in $Data.StepResults) { $dotClass = switch ($step.Status) { 'Success' { 'success' } 'Failed' { 'failure' } 'Skipped' { 'skipped' } default { 'pending' } } $stepBadge = switch ($step.Status) { 'Success' { '<span class="badge badge-success">Success</span>' } 'Failed' { '<span class="badge badge-error">Failed</span>' } 'Skipped' { '<span class="badge badge-neutral">Skipped</span>' } default { '<span class="badge badge-info">' + $step.Status + '</span>' } } $stepOutput = '' if ($step.Output) { $escapedOutput = [System.Web.HttpUtility]::HtmlEncode(($step.Output | Out-String).Trim()) if ($escapedOutput.Length -gt 0) { $stepOutput = "<pre>$escapedOutput</pre>" } } $bodyContent += @" <div class="step-item"> <div class="step-dot $dotClass"></div> <div class="step-title">$($step.StepId) - $($step.Description) $stepBadge</div> <div class="step-detail">Action: $($step.Action) | Duration: $($step.Duration)</div> $stepOutput </div> "@ } $bodyContent += '</div></div>' } # Approval log if ($Data.ApprovalLog -and @($Data.ApprovalLog).Count -gt 0) { $bodyContent += '<div class="card"><h2>Approval Log</h2><table><tr><th>Step</th><th>Method</th><th>Approved</th><th>Approver</th><th>Time</th></tr>' foreach ($approval in $Data.ApprovalLog) { $approvedBadge = if ($approval.Approved) { '<span class="badge badge-success">Yes</span>' } else { '<span class="badge badge-error">No</span>' } $bodyContent += "<tr><td>$($approval.StepDescription)</td><td>$($approval.Method)</td><td>$approvedBadge</td><td>$($approval.Approver)</td><td>$($approval.ResponseTime)s</td></tr>" } $bodyContent += '</table></div>' } # Verification results if ($Data.VerificationResults -and @($Data.VerificationResults).Count -gt 0) { $bodyContent += '<div class="card"><h2>Verification Results</h2><table><tr><th>Step</th><th>Verified</th><th>Attempts</th><th>Result</th></tr>' foreach ($verify in $Data.VerificationResults) { $verifiedBadge = if ($verify.Verified) { '<span class="badge badge-success">Verified</span>' } else { '<span class="badge badge-error">Not Verified</span>' } $bodyContent += "<tr><td>$($verify.StepId)</td><td>$verifiedBadge</td><td>$($verify.Attempts)</td><td>$($verify.FinalResult)</td></tr>" } $bodyContent += '</table></div>' } } 'HealthScore' { if (-not $Title) { $Title = "CI Health Score Report" } $gradeClass = switch ($Data.Grade) { 'A' { 'grade-a' } 'B' { 'grade-b' } 'C' { 'grade-c' } 'D' { 'grade-d' } 'F' { 'grade-f' } default { 'grade-c' } } $trendIcon = switch ($Data.Trend) { 'Improving' { '↑' } 'Declining' { '↓' } 'Stable' { '↔' } default { '—' } } $bodyContent = @" <div class="header"> <h1>$Title</h1> <p>$($Data.ComputerName) | Generated: $timestamp</p> </div> <div class="grid"> <div class="stat-card"> <div class="score-ring $gradeClass">$($Data.Grade)</div> <div class="stat-label">Health Grade</div> </div> <div class="stat-card"> <div class="stat-value">$($Data.Score)</div> <div class="stat-label">Health Score</div> </div> <div class="stat-card"> <div class="stat-value">$trendIcon</div> <div class="stat-label">Trend: $($Data.Trend)</div> </div> </div> "@ # Factors if ($Data.Factors -and @($Data.Factors).Count -gt 0) { $bodyContent += '<div class="card"><h2>Health Factors</h2><table><tr><th>Factor</th><th>Impact</th><th>Details</th></tr>' foreach ($factor in $Data.Factors) { $impactBadge = if ($factor.Impact -lt 0) { '<span class="badge badge-error">' + $factor.Impact + '</span>' } else { '<span class="badge badge-success">+' + $factor.Impact + '</span>' } $bodyContent += "<tr><td>$($factor.Name)</td><td>$impactBadge</td><td>$($factor.Details)</td></tr>" } $bodyContent += '</table></div>' } # Recommendations if ($Data.Recommendations -and @($Data.Recommendations).Count -gt 0) { $bodyContent += '<div class="card"><h2>Recommendations</h2>' foreach ($rec in $Data.Recommendations) { $bodyContent += "<p style=`"margin-bottom: 0.5rem; padding-left: 1rem; border-left: 3px solid $accentColor;`">$rec</p>" } $bodyContent += '</div>' } } 'ShiftHandoff' { if (-not $Title) { $Title = "Shift Handoff Report" } $bodyContent = @" <div class="header"> <h1>$Title</h1> <p>Period: Last $($Data.HoursBack) hours | Generated: $timestamp</p> </div> <div class="grid"> <div class="stat-card"> <div class="stat-value">$(if ($Data.RunbookActivity) { @($Data.RunbookActivity).Count } else { 0 })</div> <div class="stat-label">Runbook Executions</div> </div> <div class="stat-card"> <div class="stat-value">$(if ($Data.Escalations) { @($Data.Escalations).Count } else { 0 })</div> <div class="stat-label">Escalations</div> </div> <div class="stat-card"> <div class="stat-value">$(if ($Data.PendingApprovals) { @($Data.PendingApprovals).Count } else { 0 })</div> <div class="stat-label">Pending Approvals</div> </div> </div> "@ # Runbook Activity if ($Data.RunbookActivity -and @($Data.RunbookActivity).Count -gt 0) { $bodyContent += '<div class="card"><h2>Runbook Activity</h2><table><tr><th>Runbook</th><th>Target</th><th>Status</th><th>Time</th></tr>' foreach ($activity in $Data.RunbookActivity) { $sBadge = switch ($activity.Status) { 'Completed' { '<span class="badge badge-success">Completed</span>' } 'Failed' { '<span class="badge badge-error">Failed</span>' } 'Escalated' { '<span class="badge badge-warning">Escalated</span>' } default { '<span class="badge badge-neutral">' + $activity.Status + '</span>' } } $bodyContent += "<tr><td>$($activity.RunbookName)</td><td>$($activity.ComputerName)</td><td>$sBadge</td><td>$($activity.StartTime)</td></tr>" } $bodyContent += '</table></div>' } # Escalations if ($Data.Escalations -and @($Data.Escalations).Count -gt 0) { $bodyContent += '<div class="card"><h2>Escalations</h2><table><tr><th>Runbook</th><th>Target</th><th>Reason</th><th>Priority</th></tr>' foreach ($esc in $Data.Escalations) { $bodyContent += "<tr><td>$($esc.RunbookName)</td><td>$($esc.ComputerName)</td><td>$($esc.Message)</td><td>$($esc.Priority)</td></tr>" } $bodyContent += '</table></div>' } # Pending Approvals if ($Data.PendingApprovals -and @($Data.PendingApprovals).Count -gt 0) { $bodyContent += '<div class="card"><h2>Pending Approvals</h2><table><tr><th>Runbook</th><th>Step</th><th>Target</th><th>Requested At</th></tr>' foreach ($pending in $Data.PendingApprovals) { $bodyContent += "<tr><td>$($pending.RunbookName)</td><td>$($pending.StepDescription)</td><td>$($pending.ComputerName)</td><td>$($pending.RequestedAt)</td></tr>" } $bodyContent += '</table></div>' } # Next Shift Notes if ($Data.NextShiftNotes -and @($Data.NextShiftNotes).Count -gt 0) { $bodyContent += '<div class="card"><h2>Notes for Next Shift</h2>' foreach ($note in $Data.NextShiftNotes) { $bodyContent += "<p style=`"margin-bottom: 0.5rem; padding-left: 1rem; border-left: 3px solid $accentColor;`">$note</p>" } $bodyContent += '</div>' } } } $html = @" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>$Title</title> <style> $css </style> </head> <body> <div class="container"> $bodyContent <div class="footer"> Generated by Infra-RunbookEngine v1.0.0 | $timestamp </div> </div> </body> </html> "@ # Ensure output directory exists $outDir = Split-Path $OutputPath -Parent if ($outDir -and -not (Test-Path $outDir)) { New-Item -Path $outDir -ItemType Directory -Force | Out-Null } $html | Set-Content -Path $OutputPath -Encoding UTF8 return $OutputPath } |