Private/New-HtmlDigest.ps1

function New-HtmlDigest {
    <#
    .SYNOPSIS
        Generates an HTML email body for the RepoWatch activity digest.
    .DESCRIPTION
        Builds a clean, email-friendly HTML document using inline CSS only.
        Renders properly in Outlook, Gmail, and Apple Mail. Shows repo activity,
        new issues, comments, PRs, star/fork changes, and PSGallery download stats.
    .PARAMETER Activity
        Array of RepoActivity objects from Get-RepoActivity.
    .PARAMETER PSGalleryStats
        Array of PSGalleryStats objects from Get-PSGalleryStats.
    .PARAMETER DigestDate
        Date string for the digest header. Defaults to current date.
    .EXAMPLE
        $html = New-HtmlDigest -Activity $activity -PSGalleryStats $stats
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [object[]]$Activity,

        [Parameter()]
        [object[]]$PSGalleryStats,

        [Parameter()]
        [string]$DigestDate = (Get-Date -Format 'yyyy-MM-dd')
    )

    $activeRepos = @()
    if ($Activity) {
        $activeRepos = @($Activity | Where-Object { $_.HasActivity })
    }

    # Calculate summary counts
    $totalNewIssues = 0
    $totalNewComments = 0
    $totalNewPRs = 0
    $totalStarsChange = 0

    foreach ($repo in $activeRepos) {
        if ($repo.NewIssues) { $totalNewIssues += @($repo.NewIssues).Count }
        if ($repo.NewComments) { $totalNewComments += @($repo.NewComments).Count }
        if ($repo.NewPRs) { $totalNewPRs += @($repo.NewPRs).Count }
        $totalStarsChange += $repo.StarsChange
    }

    $totalDownloadChange = 0
    if ($PSGalleryStats) {
        foreach ($mod in $PSGalleryStats) {
            $totalDownloadChange += $mod.DownloadChange
        }
    }

    # Build summary line
    $summaryParts = [System.Collections.Generic.List[string]]::new()
    if ($Activity) {
        $summaryParts.Add("$($activeRepos.Count) repo$(if ($activeRepos.Count -ne 1) {'s'}) with activity")
    }
    if ($totalNewIssues -gt 0) { $summaryParts.Add("$totalNewIssues new issue$(if ($totalNewIssues -ne 1) {'s'})") }
    if ($totalNewComments -gt 0) { $summaryParts.Add("$totalNewComments new comment$(if ($totalNewComments -ne 1) {'s'})") }
    if ($totalNewPRs -gt 0) { $summaryParts.Add("$totalNewPRs new PR$(if ($totalNewPRs -ne 1) {'s'})") }
    if ($totalStarsChange -gt 0) { $summaryParts.Add("+$totalStarsChange star$(if ($totalStarsChange -ne 1) {'s'})") }
    if ($totalDownloadChange -gt 0) { $summaryParts.Add("$totalDownloadChange new download$(if ($totalDownloadChange -ne 1) {'s'})") }

    $summaryLine = if ($summaryParts.Count -gt 0) { $summaryParts -join ' | ' } else { 'No new activity detected.' }

    # Helper: format delta with color
    function Format-Delta {
        param([int]$Value, [switch]$Invert)
        if ($Value -gt 0) {
            $color = if ($Invert) { '#c0392b' } else { '#27ae60' }
            return "<span style=`"color:$color;font-weight:bold;`">+$Value</span>"
        }
        elseif ($Value -lt 0) {
            $color = if ($Invert) { '#27ae60' } else { '#c0392b' }
            return "<span style=`"color:$color;font-weight:bold;`">$Value</span>"
        }
        return '<span style="color:#999;">0</span>'
    }

    # Helper: HTML-encode text
    function Encode-Html {
        param([string]$Text)
        if (-not $Text) { return '' }
        return [System.Net.WebUtility]::HtmlEncode($Text)
    }

    # Start building HTML
    $html = [System.Text.StringBuilder]::new()
    [void]$html.AppendLine('<!DOCTYPE html>')
    [void]$html.AppendLine('<html lang="en">')
    [void]$html.AppendLine('<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>')
    [void]$html.AppendLine('<body style="margin:0;padding:0;background-color:#f5f5f5;font-family:Segoe UI,Helvetica,Arial,sans-serif;font-size:14px;color:#333;">')

    # Container
    [void]$html.AppendLine('<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background-color:#f5f5f5;"><tr><td align="center" style="padding:20px 10px;">')
    [void]$html.AppendLine('<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="640" style="background-color:#ffffff;border:1px solid #e0e0e0;border-radius:4px;">')

    # Header
    [void]$html.AppendLine('<tr><td style="background-color:#24292e;color:#ffffff;padding:20px 30px;border-radius:4px 4px 0 0;">')
    [void]$html.AppendLine("<h1 style=`"margin:0;font-size:22px;font-weight:600;`">GitHub RepoWatch Digest</h1>")
    [void]$html.AppendLine("<p style=`"margin:5px 0 0 0;font-size:13px;color:#8b949e;`">$DigestDate</p>")
    [void]$html.AppendLine('</td></tr>')

    # Summary bar
    [void]$html.AppendLine('<tr><td style="background-color:#f0f3f6;padding:12px 30px;border-bottom:1px solid #e0e0e0;">')
    [void]$html.AppendLine("<p style=`"margin:0;font-size:13px;color:#57606a;`">$summaryLine</p>")
    [void]$html.AppendLine('</td></tr>')

    # Repo activity sections
    if ($activeRepos.Count -gt 0) {
        foreach ($repo in $activeRepos) {
            $repoNameEncoded = Encode-Html $repo.RepoName
            $starsHtml = "$(Encode-Html $repo.Stars) stars"
            if ($repo.StarsChange -ne 0) { $starsHtml += " ($(Format-Delta $repo.StarsChange))" }
            $forksHtml = "$(Encode-Html $repo.Forks) forks"
            if ($repo.ForksChange -ne 0) { $forksHtml += " ($(Format-Delta $repo.ForksChange))" }

            [void]$html.AppendLine('<tr><td style="padding:20px 30px;border-bottom:1px solid #e8e8e8;">')
            [void]$html.AppendLine("<h2 style=`"margin:0 0 6px 0;font-size:18px;`"><a href=`"$(Encode-Html $repo.RepoUrl)`" style=`"color:#0969da;text-decoration:none;`">$repoNameEncoded</a></h2>")
            [void]$html.AppendLine("<p style=`"margin:0 0 12px 0;font-size:12px;color:#57606a;`">$starsHtml &nbsp;&middot;&nbsp; $forksHtml</p>")

            # New Issues
            if ($repo.NewIssues -and @($repo.NewIssues).Count -gt 0) {
                [void]$html.AppendLine("<h3 style=`"margin:0 0 8px 0;font-size:14px;color:#c0392b;`">New Issues ($(@($repo.NewIssues).Count))</h3>")
                [void]$html.AppendLine('<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom:12px;">')
                [void]$html.AppendLine('<tr style="background-color:#f6f8fa;"><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">#</td><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">Title</td><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">Author</td><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">Date</td></tr>')
                foreach ($issue in $repo.NewIssues) {
                    $issueDate = if ($issue.created_at) { ([datetime]$issue.created_at).ToString('MMM dd HH:mm') } else { '' }
                    [void]$html.AppendLine("<tr><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;`">#$(Encode-Html ([string]$issue.number))</td><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;`"><a href=`"$(Encode-Html $issue.url)`" style=`"color:#0969da;text-decoration:none;`">$(Encode-Html $issue.title)</a></td><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;color:#57606a;`">$(Encode-Html $issue.user)</td><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;color:#57606a;white-space:nowrap;`">$issueDate</td></tr>")
                }
                [void]$html.AppendLine('</table>')
            }

            # New Comments
            if ($repo.NewComments -and @($repo.NewComments).Count -gt 0) {
                [void]$html.AppendLine("<h3 style=`"margin:0 0 8px 0;font-size:14px;color:#57606a;`">New Comments ($(@($repo.NewComments).Count))</h3>")
                [void]$html.AppendLine('<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom:12px;">')
                [void]$html.AppendLine('<tr style="background-color:#f6f8fa;"><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">Issue</td><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">By</td><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">Comment</td></tr>')
                foreach ($comment in $repo.NewComments) {
                    $preview = Encode-Html $comment.body_preview
                    [void]$html.AppendLine("<tr><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;white-space:nowrap;`"><a href=`"$(Encode-Html $comment.url)`" style=`"color:#0969da;text-decoration:none;`">#$(Encode-Html ([string]$comment.issue_number))</a></td><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;color:#57606a;white-space:nowrap;`">$(Encode-Html $comment.user)</td><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;`">$preview</td></tr>")
                }
                [void]$html.AppendLine('</table>')
            }

            # New PRs
            if ($repo.NewPRs -and @($repo.NewPRs).Count -gt 0) {
                [void]$html.AppendLine("<h3 style=`"margin:0 0 8px 0;font-size:14px;color:#2e7d32;`">New Pull Requests ($(@($repo.NewPRs).Count))</h3>")
                [void]$html.AppendLine('<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom:12px;">')
                [void]$html.AppendLine('<tr style="background-color:#f6f8fa;"><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">#</td><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">Title</td><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">Author</td><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">Date</td></tr>')
                foreach ($pr in $repo.NewPRs) {
                    $prDate = if ($pr.created_at) { ([datetime]$pr.created_at).ToString('MMM dd HH:mm') } else { '' }
                    [void]$html.AppendLine("<tr><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;`">#$(Encode-Html ([string]$pr.number))</td><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;`"><a href=`"$(Encode-Html $pr.url)`" style=`"color:#0969da;text-decoration:none;`">$(Encode-Html $pr.title)</a></td><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;color:#57606a;`">$(Encode-Html $pr.user)</td><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;color:#57606a;white-space:nowrap;`">$prDate</td></tr>")
                }
                [void]$html.AppendLine('</table>')
            }

            # Updated Issues (not new)
            if ($repo.UpdatedIssues -and @($repo.UpdatedIssues).Count -gt 0) {
                [void]$html.AppendLine("<h3 style=`"margin:0 0 8px 0;font-size:14px;color:#8a6d3b;`">Updated Issues ($(@($repo.UpdatedIssues).Count))</h3>")
                [void]$html.AppendLine('<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom:12px;">')
                [void]$html.AppendLine('<tr style="background-color:#f6f8fa;"><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">#</td><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">Title</td><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">Author</td></tr>')
                foreach ($upd in $repo.UpdatedIssues) {
                    [void]$html.AppendLine("<tr><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;`">#$(Encode-Html ([string]$upd.number))</td><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;`"><a href=`"$(Encode-Html $upd.url)`" style=`"color:#0969da;text-decoration:none;`">$(Encode-Html $upd.title)</a></td><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;color:#57606a;`">$(Encode-Html $upd.user)</td></tr>")
                }
                [void]$html.AppendLine('</table>')
            }

            [void]$html.AppendLine('</td></tr>')
        }
    }
    elseif (-not $PSGalleryStats -or @($PSGalleryStats).Count -eq 0) {
        [void]$html.AppendLine('<tr><td style="padding:30px;text-align:center;color:#57606a;">')
        [void]$html.AppendLine('<p style="margin:0;font-size:14px;">No new activity detected across your repositories.</p>')
        [void]$html.AppendLine('</td></tr>')
    }

    # PSGallery section
    if ($PSGalleryStats -and @($PSGalleryStats).Count -gt 0) {
        [void]$html.AppendLine('<tr><td style="padding:20px 30px;border-bottom:1px solid #e8e8e8;">')
        [void]$html.AppendLine("<h2 style=`"margin:0 0 12px 0;font-size:18px;color:#5c2d91;`">PowerShell Gallery</h2>")
        [void]$html.AppendLine('<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom:4px;">')
        [void]$html.AppendLine('<tr style="background-color:#f6f8fa;"><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">Module</td><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;">Version</td><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;text-align:right;">Downloads</td><td style="padding:6px 10px;font-size:12px;font-weight:600;color:#57606a;border-bottom:1px solid #e0e0e0;text-align:right;">Change</td></tr>')
        foreach ($mod in $PSGalleryStats) {
            $changeHtml = Format-Delta $mod.DownloadChange
            $galleryUrl = if ($mod.GalleryUrl) { $mod.GalleryUrl } else { "https://www.powershellgallery.com/packages/$($mod.ModuleName)" }
            [void]$html.AppendLine("<tr><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;`"><a href=`"$(Encode-Html $galleryUrl)`" style=`"color:#0969da;text-decoration:none;`">$(Encode-Html $mod.ModuleName)</a></td><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;color:#57606a;`">$(Encode-Html $mod.Version)</td><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;text-align:right;`">$($mod.TotalDownloads.ToString('N0'))</td><td style=`"padding:6px 10px;font-size:13px;border-bottom:1px solid #f0f0f0;text-align:right;`">$changeHtml</td></tr>")
        }
        [void]$html.AppendLine('</table>')
        [void]$html.AppendLine('</td></tr>')
    }

    # Footer
    [void]$html.AppendLine('<tr><td style="padding:16px 30px;background-color:#f6f8fa;border-radius:0 0 4px 4px;border-top:1px solid #e0e0e0;">')
    [void]$html.AppendLine('<p style="margin:0;font-size:11px;color:#8b949e;text-align:center;">Generated by <a href="https://github.com/larro1991/GitHub-RepoWatch" style="color:#0969da;text-decoration:none;">GitHub-RepoWatch</a></p>')
    [void]$html.AppendLine('</td></tr>')

    # Close container
    [void]$html.AppendLine('</table></td></tr></table>')
    [void]$html.AppendLine('</body></html>')

    return $html.ToString()
}