Private/New-HtmlDashboard.ps1

function New-HtmlDashboard {
    <#
    .SYNOPSIS
        Generates a dark-themed HTML dashboard report from GPO audit findings.
 
    .DESCRIPTION
        Takes structured audit results from the GPO health check functions and renders
        them into a self-contained HTML file with a dark theme and purple accent styling.
        Each audit category becomes a collapsible section with a findings summary table.
 
        This is a private helper function used by Invoke-GPOHealthAudit. It is not
        exported from the module.
 
    .PARAMETER Title
        The report title displayed in the header. Defaults to 'GPO Health Audit Report'.
 
    .PARAMETER Sections
        An array of hashtables, each representing a report section. Expected keys:
        - Name: Section heading text
        - Summary: Brief description of what this section checks
        - Findings: Array of PSCustomObjects to render as a table
        - Status: Overall section status - 'OK', 'WARNING', or 'CRITICAL'
 
    .PARAMETER OutputPath
        Full file path for the generated HTML file.
 
    .PARAMETER DomainName
        The Active Directory domain name to display in the report header.
 
    .OUTPUTS
        [System.IO.FileInfo] The generated HTML file object.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$Title = 'GPO Health Audit Report',

        [Parameter(Mandatory)]
        [hashtable[]]$Sections,

        [Parameter(Mandatory)]
        [string]$OutputPath,

        [Parameter()]
        [string]$DomainName = $env:USERDNSDOMAIN
    )

    begin {
        Write-Verbose "Generating HTML dashboard: $OutputPath"
    }

    process {
        # --- Build summary counts ---
        $TotalFindings  = 0
        $CriticalCount  = 0
        $WarningCount   = 0
        $OkCount        = 0

        foreach ($Section in $Sections) {
            $FindingCount = @($Section.Findings).Count
            if ($Section.Status -eq 'CRITICAL') { $CriticalCount++ }
            elseif ($Section.Status -eq 'WARNING') { $WarningCount++ }
            else { $OkCount++ }
            $TotalFindings += $FindingCount
        }

        $GeneratedDate = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'

        # --- Build section HTML ---
        $SectionHtml = [System.Text.StringBuilder]::new()

        foreach ($Section in $Sections) {
            $StatusClass = switch ($Section.Status) {
                'CRITICAL' { 'status-critical' }
                'WARNING'  { 'status-warning'  }
                default    { 'status-ok'        }
            }
            $StatusIcon = switch ($Section.Status) {
                'CRITICAL' { '&#10060;' }
                'WARNING'  { '&#9888;'  }
                default    { '&#9989;'  }
            }

            [void]$SectionHtml.AppendLine(" <div class='section'>")
            [void]$SectionHtml.AppendLine(" <div class='section-header'>")
            [void]$SectionHtml.AppendLine(" <h2>$($Section.Name)</h2>")
            [void]$SectionHtml.AppendLine(" <span class='badge $StatusClass'>$StatusIcon $($Section.Status) &mdash; $(@($Section.Findings).Count) finding(s)</span>")
            [void]$SectionHtml.AppendLine(" </div>")
            [void]$SectionHtml.AppendLine(" <p class='section-summary'>$($Section.Summary)</p>")

            if (@($Section.Findings).Count -gt 0) {
                # Determine columns from the first object
                $Columns = @($Section.Findings)[0].PSObject.Properties | Select-Object -ExpandProperty Name

                [void]$SectionHtml.AppendLine(" <div class='table-wrapper'>")
                [void]$SectionHtml.AppendLine(" <table>")
                [void]$SectionHtml.AppendLine(" <thead><tr>")
                foreach ($Col in $Columns) {
                    [void]$SectionHtml.AppendLine(" <th>$Col</th>")
                }
                [void]$SectionHtml.AppendLine(" </tr></thead>")
                [void]$SectionHtml.AppendLine(" <tbody>")

                foreach ($Row in $Section.Findings) {
                    [void]$SectionHtml.AppendLine(" <tr>")
                    foreach ($Col in $Columns) {
                        $Value = $Row.$Col
                        if ($null -eq $Value) { $Value = '&mdash;' }

                        # Highlight finding severity in cells
                        $CellClass = ''
                        if ($Col -eq 'Finding') {
                            $CellClass = switch -Wildcard ($Value) {
                                '*CRITICAL*' { " class='cell-critical'" }
                                '*WARNING*'  { " class='cell-warning'"  }
                                '*UNLINKED*' { " class='cell-warning'"  }
                                '*EMPTY*'    { " class='cell-warning'"  }
                                '*STALE*'    { " class='cell-warning'"  }
                                default      { '' }
                            }
                        }
                        [void]$SectionHtml.AppendLine(" <td$CellClass>$Value</td>")
                    }
                    [void]$SectionHtml.AppendLine(" </tr>")
                }

                [void]$SectionHtml.AppendLine(" </tbody>")
                [void]$SectionHtml.AppendLine(" </table>")
                [void]$SectionHtml.AppendLine(" </div>")
            }
            else {
                [void]$SectionHtml.AppendLine(" <p class='no-findings'>No issues detected.</p>")
            }

            [void]$SectionHtml.AppendLine(" </div>")
        }

        # --- Assemble full HTML ---
        $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>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif;
            background: #0d1117;
            color: #c9d1d9;
            line-height: 1.6;
            padding: 2rem;
        }
        .dashboard { max-width: 1400px; margin: 0 auto; }
        .header {
            background: linear-gradient(135deg, #161b22 0%, #1a1230 100%);
            border: 1px solid #a371f7;
            border-radius: 8px;
            padding: 2rem;
            margin-bottom: 2rem;
            text-align: center;
        }
        .header h1 { color: #a371f7; font-size: 1.8rem; margin-bottom: 0.5rem; }
        .header .subtitle { color: #8b949e; font-size: 0.95rem; }
        .summary-bar {
            display: flex;
            gap: 1rem;
            margin-bottom: 2rem;
            flex-wrap: wrap;
        }
        .summary-card {
            flex: 1;
            min-width: 180px;
            background: #161b22;
            border: 1px solid #30363d;
            border-radius: 8px;
            padding: 1.2rem;
            text-align: center;
        }
        .summary-card .label { color: #8b949e; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; }
        .summary-card .value { font-size: 2rem; font-weight: 700; margin-top: 0.3rem; }
        .summary-card .value.critical { color: #f85149; }
        .summary-card .value.warning { color: #d29922; }
        .summary-card .value.ok { color: #3fb950; }
        .summary-card .value.total { color: #a371f7; }
        .section {
            background: #161b22;
            border: 1px solid #30363d;
            border-radius: 8px;
            padding: 1.5rem;
            margin-bottom: 1.5rem;
        }
        .section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem; flex-wrap: wrap; gap: 0.5rem; }
        .section-header h2 { color: #a371f7; font-size: 1.25rem; }
        .section-summary { color: #8b949e; font-size: 0.9rem; margin-bottom: 1rem; }
        .badge {
            padding: 0.3rem 0.8rem;
            border-radius: 20px;
            font-size: 0.8rem;
            font-weight: 600;
        }
        .status-critical { background: rgba(248, 81, 73, 0.15); color: #f85149; border: 1px solid #f8514966; }
        .status-warning { background: rgba(210, 153, 34, 0.15); color: #d29922; border: 1px solid #d2992266; }
        .status-ok { background: rgba(63, 185, 80, 0.15); color: #3fb950; border: 1px solid #3fb95066; }
        .table-wrapper { overflow-x: auto; }
        table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
        thead th {
            background: #21262d;
            color: #a371f7;
            padding: 0.6rem 0.8rem;
            text-align: left;
            border-bottom: 2px solid #a371f733;
            white-space: nowrap;
        }
        tbody td {
            padding: 0.55rem 0.8rem;
            border-bottom: 1px solid #21262d;
        }
        tbody tr:hover { background: #1c2129; }
        .cell-critical { color: #f85149; font-weight: 600; }
        .cell-warning { color: #d29922; font-weight: 600; }
        .no-findings {
            color: #3fb950;
            font-style: italic;
            padding: 1rem 0;
        }
        .footer {
            text-align: center;
            color: #484f58;
            font-size: 0.8rem;
            margin-top: 2rem;
            padding-top: 1rem;
            border-top: 1px solid #21262d;
        }
        @media (max-width: 768px) {
            body { padding: 1rem; }
            .summary-bar { flex-direction: column; }
        }
    </style>
</head>
<body>
    <div class="dashboard">
        <div class="header">
            <h1>$Title</h1>
            <div class="subtitle">Domain: $DomainName &mdash; Generated: $GeneratedDate</div>
        </div>
 
        <div class="summary-bar">
            <div class="summary-card">
                <div class="label">Total Findings</div>
                <div class="value total">$TotalFindings</div>
            </div>
            <div class="summary-card">
                <div class="label">Critical Sections</div>
                <div class="value critical">$CriticalCount</div>
            </div>
            <div class="summary-card">
                <div class="label">Warning Sections</div>
                <div class="value warning">$WarningCount</div>
            </div>
            <div class="summary-card">
                <div class="label">OK Sections</div>
                <div class="value ok">$OkCount</div>
            </div>
        </div>
 
$($SectionHtml.ToString())
        <div class="footer">
            GPO-HealthAudit v1.0.0 &mdash; Read-only audit, no GPOs were modified &mdash; $GeneratedDate
        </div>
    </div>
</body>
</html>
"@


        try {
            $Html | Out-File -FilePath $OutputPath -Encoding UTF8 -Force
            $Result = Get-Item -Path $OutputPath
            Write-Verbose "Dashboard written to: $($Result.FullName) ($([math]::Round($Result.Length / 1KB, 1)) KB)"
            return $Result
        }
        catch {
            Write-Error "Failed to write HTML dashboard to '$OutputPath': $_"
        }
    }
}