Public/Get-ShiftHandoff.ps1

function Get-ShiftHandoff {
    <#
    .SYNOPSIS
        Generates a shift handoff report summarizing runbook activity, escalations, and pending approvals.
    .DESCRIPTION
        Collects information from the engine's execution history for a specified time window:
        - Which runbooks ran and their outcomes
        - What succeeded and what failed
        - Pending approval requests
        - Escalations that occurred
        - Notes and recommendations for the incoming shift
 
        Optionally generates an HTML report and sends it via email.
    .PARAMETER HoursBack
        Number of hours to look back for the report. Default is 8 (one shift).
    .PARAMETER OutputPath
        File path to save the HTML handoff report.
    .PARAMETER IncludeRunbookActivity
        Include runbook execution activity in the report. Default is true.
    .PARAMETER IncludeAlerts
        Include alerts and escalations. Default is true.
    .PARAMETER IncludePendingApprovals
        Include any pending approval requests. Default is true.
    .PARAMETER SendEmail
        Send the report via email.
    .PARAMETER SmtpServer
        SMTP server for email delivery.
    .PARAMETER EmailTo
        Recipient email address(es).
    .PARAMETER EmailFrom
        Sender email address.
    .PARAMETER SmtpCredential
        Credential for SMTP authentication.
    .PARAMETER UseSsl
        Use SSL for SMTP connection.
    .EXAMPLE
        Get-ShiftHandoff -HoursBack 8 -OutputPath 'C:\Reports\handoff.html'
        Generate an 8-hour shift handoff report and save as HTML.
    .EXAMPLE
        Get-ShiftHandoff -HoursBack 12 -SendEmail -SmtpServer 'mail.contoso.com' -EmailTo 'nightshift@contoso.com' -EmailFrom 'runbooks@contoso.com'
        Generate and email a 12-hour handoff report.
    .EXAMPLE
        Get-ShiftHandoff -IncludeRunbookActivity -IncludePendingApprovals
        Quick console summary of runbook activity and pending approvals.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [int]$HoursBack = 8,

        [Parameter()]
        [string]$OutputPath,

        [Parameter()]
        [bool]$IncludeRunbookActivity = $true,

        [Parameter()]
        [bool]$IncludeAlerts = $true,

        [Parameter()]
        [bool]$IncludePendingApprovals = $true,

        [Parameter()]
        [switch]$SendEmail,

        [Parameter()]
        [string]$SmtpServer,

        [Parameter()]
        [string[]]$EmailTo,

        [Parameter()]
        [string]$EmailFrom,

        [Parameter()]
        [PSCredential]$SmtpCredential,

        [Parameter()]
        [switch]$UseSsl
    )

    $cutoffTime = (Get-Date).AddHours(-$HoursBack)
    $executionsPath = Join-Path $env:USERPROFILE '.runbookengine\executions'

    $runbookActivity = [System.Collections.Generic.List[object]]::new()
    $escalations = [System.Collections.Generic.List[object]]::new()
    $pendingApprovals = [System.Collections.Generic.List[object]]::new()
    $nextShiftNotes = [System.Collections.Generic.List[string]]::new()

    # Collect runbook activity
    if ($IncludeRunbookActivity -and (Test-Path $executionsPath)) {
        $execFiles = Get-ChildItem -Path $executionsPath -Filter '*.json' -ErrorAction SilentlyContinue |
            Where-Object { $_.LastWriteTime -ge $cutoffTime }

        foreach ($file in $execFiles) {
            try {
                $exec = Get-Content -Path $file.FullName -Raw | ConvertFrom-Json -ErrorAction Stop

                $execTime = if ($exec.StartTime) {
                    try { [DateTime]::Parse($exec.StartTime) } catch { $file.LastWriteTime }
                }
                else { $file.LastWriteTime }

                if ($execTime -ge $cutoffTime) {
                    $computerName = if ($exec.ComputerName) { $exec.ComputerName }
                        elseif ($exec.Parameters -and $exec.Parameters.ComputerName) { $exec.Parameters.ComputerName }
                        else { 'Unknown' }

                    $runbookActivity.Add([PSCustomObject]@{
                        ExecutionId  = $exec.ExecutionId
                        RunbookName  = $exec.RunbookName
                        ComputerName = $computerName
                        Status       = $exec.Status
                        StartTime    = $exec.StartTime
                        Duration     = $exec.Duration
                        StepCount    = if ($exec.StepResults) { @($exec.StepResults).Count } else { 0 }
                    })

                    # Check for escalations
                    if ($exec.Status -eq 'Escalated' -and $IncludeAlerts) {
                        $escalationStep = $exec.StepResults | Where-Object { $_.Action -eq 'escalate' } | Select-Object -First 1
                        $escalations.Add([PSCustomObject]@{
                            ExecutionId  = $exec.ExecutionId
                            RunbookName  = $exec.RunbookName
                            ComputerName = $computerName
                            Message      = if ($escalationStep) { $escalationStep.Output } else { 'Escalated' }
                            Priority     = 'High'
                            Time         = $exec.StartTime
                        })

                        $nextShiftNotes.Add("ESCALATION: $($exec.RunbookName) on $computerName was escalated. Review required.")
                    }

                    # Check for failures
                    if ($exec.Status -eq 'Failed') {
                        $failedStep = $exec.StepResults | Where-Object { $_.Status -eq 'Failed' } | Select-Object -First 1
                        $nextShiftNotes.Add("FAILED: $($exec.RunbookName) on $computerName failed at step '$($failedStep.StepId)'. May need manual intervention.")
                    }

                    # Check for pending approvals
                    if ($IncludePendingApprovals -and $exec.ApprovalLog) {
                        $pending = $exec.ApprovalLog | Where-Object { -not $_.Approved -and $_.Notes -match 'awaiting|pending' }
                        foreach ($p in $pending) {
                            $pendingApprovals.Add([PSCustomObject]@{
                                ExecutionId     = $exec.ExecutionId
                                RunbookName     = $exec.RunbookName
                                StepDescription = $p.StepDescription
                                ComputerName    = $computerName
                                RequestedAt     = $p.RequestedAt
                                Method          = $p.Method
                            })
                        }
                    }

                    # Check for verification failures
                    if ($exec.VerificationResults) {
                        $unverified = $exec.VerificationResults | Where-Object { -not $_.Verified }
                        foreach ($uv in $unverified) {
                            $nextShiftNotes.Add("UNVERIFIED FIX: $($exec.RunbookName) step '$($uv.StepId)' on $computerName - fix could not be verified after $($uv.Attempts) attempts.")
                        }
                    }
                }
            }
            catch {
                Write-Verbose "Failed to parse execution file $($file.Name): $_"
            }
        }
    }

    # Summary statistics
    $totalRuns = $runbookActivity.Count
    $completedRuns = @($runbookActivity | Where-Object { $_.Status -eq 'Completed' }).Count
    $failedRuns = @($runbookActivity | Where-Object { $_.Status -eq 'Failed' }).Count
    $escalatedRuns = @($runbookActivity | Where-Object { $_.Status -eq 'Escalated' }).Count

    if ($totalRuns -gt 0) {
        $nextShiftNotes.Insert(0, "Summary: $totalRuns runbook executions in the last $HoursBack hours - $completedRuns completed, $failedRuns failed, $escalatedRuns escalated.")
    }
    else {
        $nextShiftNotes.Add("No runbook activity in the last $HoursBack hours.")
    }

    if ($pendingApprovals.Count -gt 0) {
        $nextShiftNotes.Add("ACTION REQUIRED: $($pendingApprovals.Count) pending approval(s) need attention.")
    }

    # Check correlations across recent activity
    $affectedServers = $runbookActivity | ForEach-Object { $_.ComputerName } | Select-Object -Unique
    foreach ($server in $affectedServers) {
        $serverRuns = @($runbookActivity | Where-Object { $_.ComputerName -eq $server })
        if ($serverRuns.Count -ge 3) {
            $nextShiftNotes.Add("PATTERN: $server had $($serverRuns.Count) runbook executions. Multiple issues may indicate a systemic problem.")
        }
    }

    $handoffReport = [PSCustomObject]@{
        GeneratedAt       = (Get-Date).ToString('o')
        HoursBack         = $HoursBack
        PeriodStart       = $cutoffTime.ToString('o')
        PeriodEnd         = (Get-Date).ToString('o')
        TotalExecutions   = $totalRuns
        Completed         = $completedRuns
        Failed            = $failedRuns
        Escalated         = $escalatedRuns
        RunbookActivity   = $runbookActivity.ToArray()
        Escalations       = $escalations.ToArray()
        PendingApprovals  = $pendingApprovals.ToArray()
        NextShiftNotes    = $nextShiftNotes.ToArray()
        AffectedServers   = $affectedServers
    }

    # Generate HTML report
    if ($OutputPath) {
        $htmlPath = New-HtmlDashboard -ReportType 'ShiftHandoff' -Data $handoffReport -OutputPath $OutputPath
        Write-Host "Shift handoff report saved: $htmlPath" -ForegroundColor Green
    }

    # Send email if requested
    if ($SendEmail) {
        if (-not $SmtpServer) { throw "SmtpServer is required when using -SendEmail." }
        if (-not $EmailTo) { throw "EmailTo is required when using -SendEmail." }
        if (-not $EmailFrom) { $EmailFrom = "runbook-engine@$($env:USERDNSDOMAIN)" }

        $subject = "Shift Handoff Report - $(Get-Date -Format 'yyyy-MM-dd HH:mm') ($totalRuns runs, $failedRuns failed, $escalatedRuns escalated)"

        $emailBody = @"
SHIFT HANDOFF REPORT
====================
Period: Last $HoursBack hours ($(($cutoffTime).ToString('HH:mm')) to $((Get-Date).ToString('HH:mm')))
Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
 
SUMMARY
-------
Total Executions: $totalRuns
  Completed: $completedRuns
  Failed: $failedRuns
  Escalated: $escalatedRuns
  Pending Approvals: $($pendingApprovals.Count)
 
NOTES FOR NEXT SHIFT
--------------------
$($nextShiftNotes | ForEach-Object { "- $_" } | Out-String)
 
RUNBOOK ACTIVITY
----------------
$($runbookActivity | Format-Table RunbookName, ComputerName, Status, Duration -AutoSize | Out-String)
 
---
Generated by Infra-RunbookEngine
"@


        $mailParams = @{
            From       = $EmailFrom
            To         = $EmailTo
            Subject    = $subject
            Body       = $emailBody
            SmtpServer = $SmtpServer
        }

        if ($SmtpCredential) { $mailParams['Credential'] = $SmtpCredential }
        if ($UseSsl) { $mailParams['UseSsl'] = $true }

        # If HTML report was generated, attach it
        if ($OutputPath -and (Test-Path $OutputPath)) {
            $mailParams['Attachments'] = $OutputPath
        }

        try {
            Send-MailMessage @mailParams -ErrorAction Stop
            Write-Host "Handoff report emailed to: $($EmailTo -join ', ')" -ForegroundColor Green
        }
        catch {
            Write-Warning "Failed to send handoff email: $_"
        }
    }

    return $handoffReport
}