Private/ConvertTo-SDHtmlReport.ps1

function ConvertTo-SDHtmlReport {
    <#
    .SYNOPSIS
        Builds a self-contained HTML report (inline CSS, no external resources) from report rows.
    .DESCRIPTION
        Every value is HTML-encoded. Rows whose -HighlightProperty is $true get the CSS class "flag".
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Title,

        [AllowNull()]
        [AllowEmptyCollection()]
        [object[]]$InputObject = @(),

        [Parameter(Mandatory = $true)]
        [string[]]$Property,

        [System.Collections.IDictionary]$Summary,

        [string]$Description = '',

        [string]$HighlightProperty,

        [datetime]$GeneratedAt = (Get-Date)
    )

    $encode = { param($Text) [System.Net.WebUtility]::HtmlEncode([string]$Text) }
    $rows = @($InputObject | Where-Object { $null -ne $_ })
    $version = '0.0.0'
    if ($MyInvocation.MyCommand.Module) { $version = [string]$MyInvocation.MyCommand.Module.Version }

    $sb = New-Object -TypeName System.Text.StringBuilder
    [void]$sb.AppendLine('<!DOCTYPE html>')
    [void]$sb.AppendLine('<html lang="en">')
    [void]$sb.AppendLine('<head>')
    [void]$sb.AppendLine('<meta charset="utf-8">')
    [void]$sb.AppendLine('<meta name="viewport" content="width=device-width, initial-scale=1">')
    [void]$sb.AppendLine(('<title>{0}</title>' -f (& $encode $Title)))
    [void]$sb.AppendLine(@'
<style>
:root { --bg:#ffffff; --fg:#1f2328; --muted:#59636e; --line:#d1d9e0; --head:#f6f8fa; --zebra:#fbfcfd; --flag:#fff4e5; --accent:#0969da; }
@media (prefers-color-scheme: dark) {
  :root { --bg:#0d1117; --fg:#e6edf3; --muted:#9198a1; --line:#30363d; --head:#161b22; --zebra:#11161d; --flag:#3a2a12; --accent:#4493f8; }
}
* { box-sizing: border-box; }
body { margin:0; padding:24px 16px; background:var(--bg); color:var(--fg); font:14px/1.45 "Segoe UI", system-ui, -apple-system, sans-serif; }
h1 { font-size:22px; margin:0 0 4px; }
p.meta { color:var(--muted); margin:0 0 16px; }
.cards { display:flex; flex-wrap:wrap; gap:12px; margin:0 0 20px; padding:0; }
.card { border:1px solid var(--line); border-radius:8px; padding:10px 14px; min-width:140px; }
.card dt { color:var(--muted); font-size:12px; }
.card dd { margin:2px 0 0; font-size:20px; font-weight:600; font-variant-numeric:tabular-nums; }
.wrap { overflow-x:auto; border:1px solid var(--line); border-radius:8px; }
table { border-collapse:collapse; width:100%; }
th, td { text-align:left; padding:6px 10px; border-bottom:1px solid var(--line); white-space:nowrap; }
th { position:sticky; top:0; background:var(--head); font-weight:600; }
tbody tr:nth-child(even) { background:var(--zebra); }
tr.flag td { background:var(--flag); }
.empty { padding:16px; color:var(--muted); }
footer { color:var(--muted); font-size:12px; margin-top:16px; }
</style>
'@
)
    [void]$sb.AppendLine('</head>')
    [void]$sb.AppendLine('<body>')
    [void]$sb.AppendLine(('<h1>{0}</h1>' -f (& $encode $Title)))
    if ($Description) {
        [void]$sb.AppendLine(('<p class="meta">{0}</p>' -f (& $encode $Description)))
    }

    if ($Summary -and $Summary.Count -gt 0) {
        [void]$sb.AppendLine('<dl class="cards">')
        foreach ($key in $Summary.Keys) {
            [void]$sb.AppendLine(('<div class="card"><dt>{0}</dt><dd>{1}</dd></div>' -f (& $encode $key), (& $encode (Format-SDCellValue -Value $Summary[$key]))))
        }
        [void]$sb.AppendLine('</dl>')
    }

    [void]$sb.AppendLine('<div class="wrap">')
    if ($rows.Count -eq 0) {
        [void]$sb.AppendLine('<p class="empty">No entries found.</p>')
    }
    else {
        [void]$sb.AppendLine('<table>')
        [void]$sb.Append('<thead><tr>')
        foreach ($name in $Property) { [void]$sb.Append(('<th scope="col">{0}</th>' -f (& $encode $name))) }
        [void]$sb.AppendLine('</tr></thead>')
        [void]$sb.AppendLine('<tbody>')
        foreach ($row in $rows) {
            $flagged = $HighlightProperty -and ((Get-SDPropertyValue -InputObject $row -Name $HighlightProperty) -eq $true)
            if ($flagged) { [void]$sb.Append('<tr class="flag">') } else { [void]$sb.Append('<tr>') }
            foreach ($name in $Property) {
                $cell = Format-SDCellValue -Value (Get-SDPropertyValue -InputObject $row -Name $name)
                [void]$sb.Append(('<td>{0}</td>' -f (& $encode $cell)))
            }
            [void]$sb.AppendLine('</tr>')
        }
        [void]$sb.AppendLine('</tbody>')
        [void]$sb.AppendLine('</table>')
    }
    [void]$sb.AppendLine('</div>')
    [void]$sb.AppendLine(('<footer>Generated {0} by ServiceDeskToolkit {1}. {2} row(s).</footer>' -f (& $encode $GeneratedAt.ToString('yyyy-MM-dd HH:mm', [Globalization.CultureInfo]::InvariantCulture)), (& $encode $version), $rows.Count))
    [void]$sb.AppendLine('</body>')
    [void]$sb.AppendLine('</html>')
    $sb.ToString()
}