Private/New-HtmlDashboard.ps1

function New-HtmlDashboard {
    <#
    .SYNOPSIS
        Generates a dark theme HTML drift report with violet/purple accent.
    .DESCRIPTION
        Creates a comprehensive HTML dashboard showing documentation drift analysis
        with donut chart visualization, summary cards, drift details, verified facts,
        and coverage gaps.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [PSCustomObject]$FactsDatabase,

        [Parameter()]
        [string]$OutputPath,

        [Parameter()]
        [string]$Title = 'Documentation Drift Report'
    )

    $reportDate = Get-Date -Format 'yyyy-MM-dd HH:mm'
    $meta = $FactsDatabase.metadata
    $facts = $FactsDatabase.facts

    # Aggregate claim counts
    $allClaims = @()
    foreach ($fact in $facts) {
        foreach ($claim in $fact.claims) {
            $allClaims += [PSCustomObject]@{
                FactId          = $fact.id
                SourceDocument  = $fact.source_document
                SourceText      = $fact.source_text
                Category        = $fact.category
                ClaimType       = $claim.claim_type
                Subject         = $claim.subject
                ExpectedValue   = $claim.expected_value
                ActualValue     = $claim.actual_value
                Status          = $claim.status
                Method          = $claim.verification_method
                LastChecked     = $claim.last_checked
            }
        }
    }

    $verified = @($allClaims | Where-Object { $_.Status -eq 'verified' })
    $drift = @($allClaims | Where-Object { $_.Status -eq 'drift' })
    $unreachable = @($allClaims | Where-Object { $_.Status -eq 'unreachable' })
    $unverifiable = @($allClaims | Where-Object { $_.Status -eq 'unverifiable' })
    $pending = @($allClaims | Where-Object { $_.Status -eq 'pending' })
    $total = $allClaims.Count

    # Calculate percentages for donut chart
    $pctVerified = if ($total -gt 0) { [Math]::Round(($verified.Count / $total) * 100) } else { 0 }
    $pctDrift = if ($total -gt 0) { [Math]::Round(($drift.Count / $total) * 100) } else { 0 }
    $pctUnreachable = if ($total -gt 0) { [Math]::Round(($unreachable.Count / $total) * 100) } else { 0 }
    $pctUnverifiable = if ($total -gt 0) { [Math]::Round(($unverifiable.Count / $total) * 100) } else { 0 }
    $pctPending = if ($total -gt 0) { 100 - $pctVerified - $pctDrift - $pctUnreachable - $pctUnverifiable } else { 0 }

    # Build drift rows
    $driftRowsHtml = ''
    foreach ($d in $drift) {
        $driftRowsHtml += @"
            <tr>
                <td><span class="category-badge">$($d.Category)</span></td>
                <td><strong>$([System.Web.HttpUtility]::HtmlEncode($d.Subject))</strong></td>
                <td>$([System.Web.HttpUtility]::HtmlEncode($d.ClaimType))</td>
                <td class="drift-expected">$([System.Web.HttpUtility]::HtmlEncode($d.ExpectedValue))</td>
                <td class="drift-actual">$([System.Web.HttpUtility]::HtmlEncode($d.ActualValue))</td>
                <td class="source-doc">$([System.Web.HttpUtility]::HtmlEncode($d.SourceDocument))</td>
            </tr>
"@

    }

    # Build verified rows
    $verifiedRowsHtml = ''
    foreach ($v in $verified) {
        $verifiedRowsHtml += @"
            <tr>
                <td><span class="category-badge">$($v.Category)</span></td>
                <td><strong>$([System.Web.HttpUtility]::HtmlEncode($v.Subject))</strong></td>
                <td>$([System.Web.HttpUtility]::HtmlEncode($v.ClaimType))</td>
                <td>$([System.Web.HttpUtility]::HtmlEncode($v.ExpectedValue))</td>
                <td class="verified-check">&#10003;</td>
                <td class="source-doc">$([System.Web.HttpUtility]::HtmlEncode($v.SourceDocument))</td>
            </tr>
"@

    }

    # Build unreachable rows
    $unreachableRowsHtml = ''
    foreach ($u in $unreachable) {
        $unreachableRowsHtml += @"
            <tr>
                <td><span class="category-badge">$($u.Category)</span></td>
                <td><strong>$([System.Web.HttpUtility]::HtmlEncode($u.Subject))</strong></td>
                <td>$([System.Web.HttpUtility]::HtmlEncode($u.ClaimType))</td>
                <td>$([System.Web.HttpUtility]::HtmlEncode($u.ExpectedValue))</td>
                <td>$([System.Web.HttpUtility]::HtmlEncode($u.ActualValue))</td>
                <td class="source-doc">$([System.Web.HttpUtility]::HtmlEncode($u.SourceDocument))</td>
            </tr>
"@

    }

    # Source documents list
    $sourceDocsHtml = ''
    $sourceGroups = $allClaims | Group-Object -Property SourceDocument
    foreach ($sg in $sourceGroups) {
        $sgVerified = @($sg.Group | Where-Object { $_.Status -eq 'verified' }).Count
        $sgDrift = @($sg.Group | Where-Object { $_.Status -eq 'drift' }).Count
        $sgTotal = $sg.Count
        $sourceDocsHtml += @"
            <div class="source-card">
                <h4>$([System.Web.HttpUtility]::HtmlEncode($sg.Name))</h4>
                <p>$sgTotal claims &mdash; <span class="text-green">$sgVerified verified</span>, <span class="text-red">$sgDrift drift</span></p>
            </div>
"@

    }

    # Category breakdown
    $categoryHtml = ''
    $categoryGroups = $allClaims | Group-Object -Property Category
    foreach ($cg in $categoryGroups) {
        $cgVerified = @($cg.Group | Where-Object { $_.Status -eq 'verified' }).Count
        $cgDrift = @($cg.Group | Where-Object { $_.Status -eq 'drift' }).Count
        $categoryHtml += @"
            <div class="category-card">
                <h4>$($cg.Name)</h4>
                <div class="mini-bar">
                    <div class="mini-bar-verified" style="width: $(if($cg.Count -gt 0){[Math]::Round(($cgVerified/$cg.Count)*100)}else{0})%"></div>
                    <div class="mini-bar-drift" style="width: $(if($cg.Count -gt 0){[Math]::Round(($cgDrift/$cg.Count)*100)}else{0})%"></div>
                </div>
                <p>$($cg.Count) facts &mdash; $cgVerified ok, $cgDrift drift</p>
            </div>
"@

    }

    # Conic gradient segments for donut chart
    $segments = @()
    $runningPct = 0
    if ($pctVerified -gt 0) {
        $segments += "#3fb950 ${runningPct}% $($runningPct + $pctVerified)%"
        $runningPct += $pctVerified
    }
    if ($pctDrift -gt 0) {
        $segments += "#f85149 ${runningPct}% $($runningPct + $pctDrift)%"
        $runningPct += $pctDrift
    }
    if ($pctUnreachable -gt 0) {
        $segments += "#d29922 ${runningPct}% $($runningPct + $pctUnreachable)%"
        $runningPct += $pctUnreachable
    }
    if ($pctUnverifiable -gt 0) {
        $segments += "#8b949e ${runningPct}% $($runningPct + $pctUnverifiable)%"
        $runningPct += $pctUnverifiable
    }
    if ($pctPending -gt 0) {
        $segments += "#6e7681 ${runningPct}% 100%"
    }
    $conicGradient = if ($segments.Count -gt 0) { $segments -join ', ' } else { '#6e7681 0% 100%' }

    $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', Helvetica, Arial, sans-serif;
            background: #0d1117;
            color: #e6edf3;
            line-height: 1.6;
            padding: 2rem;
        }
        .container { max-width: 1200px; margin: 0 auto; }
        h1 {
            color: #a371f7;
            font-size: 1.8rem;
            border-bottom: 2px solid #a371f7;
            padding-bottom: 0.5rem;
            margin-bottom: 0.5rem;
        }
        .subtitle { color: #8b949e; margin-bottom: 2rem; font-size: 0.95rem; }
        h2 {
            color: #a371f7;
            font-size: 1.3rem;
            margin: 2rem 0 1rem 0;
            padding-bottom: 0.3rem;
            border-bottom: 1px solid #30363d;
        }
        h3 { color: #c9d1d9; font-size: 1.1rem; margin: 1rem 0 0.5rem 0; }
        h4 { color: #c9d1d9; font-size: 1rem; margin-bottom: 0.3rem; }
 
        /* Summary Cards */
        .summary-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
            gap: 1rem;
            margin-bottom: 2rem;
        }
        .summary-card {
            background: #161b22;
            border: 1px solid #30363d;
            border-radius: 8px;
            padding: 1.2rem;
            text-align: center;
        }
        .summary-card .number {
            font-size: 2.2rem;
            font-weight: bold;
            display: block;
        }
        .summary-card .label {
            font-size: 0.85rem;
            color: #8b949e;
            text-transform: uppercase;
            letter-spacing: 0.05em;
        }
        .card-total .number { color: #a371f7; }
        .card-verified .number { color: #3fb950; }
        .card-drift .number { color: #f85149; }
        .card-unreachable .number { color: #d29922; }
        .card-unverifiable .number { color: #8b949e; }
 
        /* Donut Chart */
        .chart-section {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 2rem;
            margin: 2rem 0;
        }
        .donut {
            width: 180px;
            height: 180px;
            border-radius: 50%;
            background: conic-gradient($conicGradient);
            position: relative;
        }
        .donut::after {
            content: '';
            position: absolute;
            top: 35px; left: 35px;
            width: 110px; height: 110px;
            border-radius: 50%;
            background: #0d1117;
        }
        .donut-center {
            position: absolute;
            top: 50%; left: 50%;
            transform: translate(-50%, -50%);
            z-index: 1;
            text-align: center;
        }
        .donut-center .pct { font-size: 1.6rem; font-weight: bold; color: #a371f7; }
        .donut-center .pct-label { font-size: 0.7rem; color: #8b949e; }
        .legend { list-style: none; }
        .legend li { margin: 0.4rem 0; font-size: 0.9rem; display: flex; align-items: center; gap: 0.5rem; }
        .legend-dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
 
        /* Tables */
        table {
            width: 100%;
            border-collapse: collapse;
            margin: 1rem 0;
            font-size: 0.9rem;
        }
        th {
            background: #161b22;
            color: #a371f7;
            text-align: left;
            padding: 0.7rem 0.8rem;
            border-bottom: 2px solid #30363d;
            font-weight: 600;
        }
        td {
            padding: 0.6rem 0.8rem;
            border-bottom: 1px solid #21262d;
        }
        tr:hover { background: #161b22; }
        .drift-expected { color: #f85149; text-decoration: line-through; }
        .drift-actual { color: #3fb950; font-weight: 600; }
        .verified-check { color: #3fb950; font-size: 1.2rem; text-align: center; }
        .source-doc { color: #8b949e; font-size: 0.8rem; }
 
        /* Badges */
        .category-badge {
            background: #30363d;
            color: #a371f7;
            padding: 0.15rem 0.5rem;
            border-radius: 12px;
            font-size: 0.75rem;
            font-weight: 600;
            text-transform: uppercase;
        }
        .status-badge {
            padding: 0.15rem 0.5rem;
            border-radius: 12px;
            font-size: 0.75rem;
            font-weight: 600;
        }
        .status-verified { background: #0f2d1a; color: #3fb950; }
        .status-drift { background: #3d1214; color: #f85149; }
        .status-unreachable { background: #3d2e00; color: #d29922; }
        .status-unverifiable { background: #21262d; color: #8b949e; }
 
        /* Source & Category cards */
        .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; }
        .source-card, .category-card {
            background: #161b22;
            border: 1px solid #30363d;
            border-radius: 8px;
            padding: 1rem;
        }
        .text-green { color: #3fb950; }
        .text-red { color: #f85149; }
 
        /* Mini progress bars */
        .mini-bar {
            height: 6px;
            background: #21262d;
            border-radius: 3px;
            display: flex;
            overflow: hidden;
            margin: 0.5rem 0;
        }
        .mini-bar-verified { background: #3fb950; }
        .mini-bar-drift { background: #f85149; }
 
        .category-card p, .source-card p { font-size: 0.85rem; color: #8b949e; }
 
        /* Section toggle (collapsible) */
        details { margin: 1rem 0; }
        summary {
            cursor: pointer;
            color: #a371f7;
            font-size: 1.1rem;
            font-weight: 600;
            padding: 0.5rem 0;
        }
        summary:hover { color: #c9a0ff; }
 
        /* Footer */
        .footer {
            margin-top: 3rem;
            padding-top: 1rem;
            border-top: 1px solid #30363d;
            text-align: center;
            color: #484f58;
            font-size: 0.8rem;
        }
 
        /* No data message */
        .no-data {
            color: #8b949e;
            font-style: italic;
            padding: 1rem;
            text-align: center;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>$Title</h1>
        <p class="subtitle">Generated: $reportDate | Source documents: $(($meta.source_documents | Measure-Object).Count)</p>
 
        <!-- Executive Summary -->
        <h2>Executive Summary</h2>
        <div class="summary-grid">
            <div class="summary-card card-total">
                <span class="number">$total</span>
                <span class="label">Total Claims</span>
            </div>
            <div class="summary-card card-verified">
                <span class="number">$($verified.Count)</span>
                <span class="label">Verified</span>
            </div>
            <div class="summary-card card-drift">
                <span class="number">$($drift.Count)</span>
                <span class="label">Drift Detected</span>
            </div>
            <div class="summary-card card-unreachable">
                <span class="number">$($unreachable.Count)</span>
                <span class="label">Unreachable</span>
            </div>
            <div class="summary-card card-unverifiable">
                <span class="number">$($unverifiable.Count)</span>
                <span class="label">Unverifiable</span>
            </div>
        </div>
 
        <!-- Donut Chart -->
        <div class="chart-section">
            <div class="donut">
                <div class="donut-center">
                    <span class="pct">$pctVerified%</span>
                    <br><span class="pct-label">ACCURATE</span>
                </div>
            </div>
            <ul class="legend">
                <li><span class="legend-dot" style="background:#3fb950"></span> Verified ($($verified.Count))</li>
                <li><span class="legend-dot" style="background:#f85149"></span> Drift ($($drift.Count))</li>
                <li><span class="legend-dot" style="background:#d29922"></span> Unreachable ($($unreachable.Count))</li>
                <li><span class="legend-dot" style="background:#8b949e"></span> Unverifiable ($($unverifiable.Count))</li>
                $(if ($pending.Count -gt 0) { "<li><span class='legend-dot' style='background:#6e7681'></span> Pending ($($pending.Count))</li>" })
            </ul>
        </div>
 
        <!-- Categories -->
        <h2>By Category</h2>
        <div class="card-grid">
            $categoryHtml
        </div>
 
        <!-- Drift Details -->
        <h2>Drift Detected</h2>
        $(if ($drift.Count -gt 0) {
            @"
        <p style="color:#8b949e;margin-bottom:1rem;">The following $($drift.Count) claims no longer match reality:</p>
        <table>
            <thead>
                <tr>
                    <th>Category</th>
                    <th>Subject</th>
                    <th>Claim</th>
                    <th>Document Says</th>
                    <th>Reality</th>
                    <th>Source</th>
                </tr>
            </thead>
            <tbody>
                $driftRowsHtml
            </tbody>
        </table>
"@
        } else {
            '<p class="no-data">No drift detected. All verified claims match the documentation.</p>'
        })
 
        <!-- Verified Facts -->
        <details>
            <summary>Verified Facts ($($verified.Count) claims confirmed accurate)</summary>
            $(if ($verified.Count -gt 0) {
                @"
            <table>
                <thead>
                    <tr>
                        <th>Category</th>
                        <th>Subject</th>
                        <th>Claim</th>
                        <th>Value</th>
                        <th>Status</th>
                        <th>Source</th>
                    </tr>
                </thead>
                <tbody>
                    $verifiedRowsHtml
                </tbody>
            </table>
"@
            } else {
                '<p class="no-data">No facts have been verified yet.</p>'
            })
        </details>
 
        <!-- Unreachable -->
        $(if ($unreachable.Count -gt 0) {
            @"
        <details>
            <summary>Unreachable ($($unreachable.Count) claims could not be checked)</summary>
            <table>
                <thead>
                    <tr>
                        <th>Category</th>
                        <th>Subject</th>
                        <th>Claim</th>
                        <th>Expected</th>
                        <th>Reason</th>
                        <th>Source</th>
                    </tr>
                </thead>
                <tbody>
                    $unreachableRowsHtml
                </tbody>
            </table>
        </details>
"@
        })
 
        <!-- Source Documents -->
        <h2>Source Documents</h2>
        <div class="card-grid">
            $sourceDocsHtml
        </div>
 
        <div class="footer">
            Generated by <strong>Infra-LivingDoc</strong> &mdash; Living Documentation for IT Infrastructure
        </div>
    </div>
</body>
</html>
"@


    # Add System.Web for HtmlEncode if not loaded
    Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue

    if ($OutputPath) {
        $parentDir = Split-Path $OutputPath -Parent
        if ($parentDir -and -not (Test-Path $parentDir)) {
            New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
        }
        $html | Out-File -FilePath $OutputPath -Encoding UTF8 -Force
        Write-Verbose "Dashboard saved to: $OutputPath"
        return Get-Item $OutputPath
    }
    else {
        return $html
    }
}