src/public/Orchestration/Get-AitherOrchestrationStatus.ps1

#Requires -Version 7.0

<#
.SYNOPSIS
    Check execution status of running orchestration
 
.DESCRIPTION
    Retrieves the current status of orchestration execution, including which scripts
    are running, completed, or failed. Useful for monitoring long-running playbooks
    and understanding execution progress.
 
.PARAMETER PlaybookName
    Name of the playbook to check status for. If not specified, checks all active orchestrations.
 
.PARAMETER ExecutionId
    Specific execution ID to check. Use this to track a specific orchestration run.
 
.INPUTS
    System.String
    You can pipe playbook names to Get-AitherOrchestrationStatus.
 
.OUTPUTS
    PSCustomObject
    Returns orchestration status with properties:
    - PlaybookName: Name of the playbook
    - ExecutionId: Unique execution identifier
    - Status: Current status (Running, Completed, Failed, Paused)
    - StartTime: When execution started
    - Duration: How long execution has been running
    - CompletedScripts: Number of scripts completed
    - TotalScripts: Total number of scripts
    - RunningScripts: Currently running scripts
    - FailedScripts: Failed scripts
 
.EXAMPLE
    Get-AitherOrchestrationStatus
 
    Gets status of all active orchestrations.
 
.EXAMPLE
    Get-AitherOrchestrationStatus -PlaybookName "deployment"
 
    Gets status of the "deployment" playbook execution.
 
.NOTES
    Orchestration status is tracked in the execution history system.
    This cmdlet provides real-time visibility into playbook execution.
 
.LINK
    Invoke-AitherPlaybook
    Get-AitherExecutionHistory
    Stop-AitherOrchestration
#>

function Get-AitherOrchestrationStatus {
    [OutputType([PSCustomObject])]
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string]$PlaybookName,

        [Parameter()]
        [string]$ExecutionId,

        [switch]$ShowOutput
    )

    begin {
        # Save original log targets
        $originalLogTargets = $script:AitherLogTargets

        # Set log targets based on ShowOutput parameter
        if ($ShowOutput) {
            # Ensure Console is in the log targets
            if ($script:AitherLogTargets -notcontains 'Console') {
                $script:AitherLogTargets += 'Console'
            }
        }
        else {
            # Remove Console from log targets if present (default behavior)
            if ($script:AitherLogTargets -contains 'Console') {
                $script:AitherLogTargets = $script:AitherLogTargets | Where-Object { $_ -ne 'Console' }
            }
        }

        $moduleRoot = Get-AitherModuleRoot
    }

    process {
        try {
            try {
                $moduleRoot = Get-AitherModuleRoot
                # Canonical store, written by Write-AitherExecutionRecord. This used to
                # read 'library/execution-history' while Get-AitherExecutionHistory read
                # 'library/reports/execution-history' -- and NOTHING wrote either one.
                $historyPath = Join-Path $moduleRoot 'library' 'reports' 'execution-history'

                if (-not (Test-Path $historyPath)) {
                    return @()
                }

                # Get execution history files
                $historyFiles = Get-ChildItem -Path $historyPath -Filter "*.json" -ErrorAction SilentlyContinue |
                Sort-Object LastWriteTime -Descending

                $statuses = @()

                foreach ($file in $historyFiles) {
                    try {
                        $execution = Get-Content -Path $file.FullName -Raw | ConvertFrom-Json -AsHashtable

                        # Apply filters
                        if ($PlaybookName -and $execution.PlaybookName -ne $PlaybookName) {
                            continue
                        }
                        if ($ExecutionId -and $execution.ExecutionId -ne $ExecutionId) {
                            continue
                        }

                        # Prefer the status the writer recorded. The derivation below
                        # keys on Completed, which is a COUNT: a run that failed at step
                        # 1 has Completed=0, which is falsy, so it reported 'Running'
                        # forever and Resume refused to touch it ("already running").
                        $status = 'Unknown'
                        if ($execution.Status) {
                            $status = $execution.Status
                        }
                        elseif ($execution.Failed -gt 0) {
                            $status = 'Failed'
                        }
                        elseif ($execution.Completed -gt 0) {
                            $status = 'Completed'
                        }
                        elseif ($execution.StartTime) {
                            $status = 'Running'
                        }

                        $statusObj = [PSCustomObject]@{
                            PSTypeName       = 'AitherZero.OrchestrationStatus'
                            PlaybookName     = $execution.PlaybookName
                            ExecutionId      = $execution.ExecutionId
                            Status           = $status
                            StartTime        = if ($execution.StartTime) { [DateTime]$execution.StartTime } else { $null }
                            Duration         = if ($execution.StartTime -and $execution.EndTime) {
                                ([DateTime]$execution.EndTime) - ([DateTime]$execution.StartTime)
                            }
                            elseif ($execution.StartTime) {
                                (Get-Date) - ([DateTime]$execution.StartTime)
                            }
                            else { $null }
                            CompletedScripts = $execution.Completed
                            TotalScripts     = $execution.Total
                            RunningScripts   = if ($execution.Running) { $execution.Running } else { 0 }
                            FailedScripts    = if ($execution.Failed) { $execution.Failed } else { 0 }
                            # Per-step detail. Resume needs to know WHICH steps
                            # completed, not how many -- with SkipFailed the completed
                            # indices are not contiguous, so a count cannot identify them.
                            Results          = if ($execution.Results) { $execution.Results } else { @() }
                        }

                        $statuses += $statusObj
                    }
                    catch {
                        continue
                    }
                }

                return $statuses
            }
            catch {
                # Use centralized error handling
                $errorScript = Join-Path $PSScriptRoot '..' 'Private' 'Write-AitherError.ps1'
                if (Test-Path $errorScript) {
                    . $errorScript -ErrorRecord $_ -CmdletName $PSCmdlet.MyInvocation.MyCommand.Name -Operation "Getting orchestration status" -Parameters $PSBoundParameters -ThrowOnError
                }
                else {
                    Write-AitherLog -Level Error -Message "Failed to get orchestration status: $_" -Source 'Get-AitherOrchestrationStatus' -Exception $_
                    throw
                }
            }
        }
        finally {
            # Restore original log targets
            $script:AitherLogTargets = $originalLogTargets
        }
    }

}