Private/System/Utilities/ReusableHelpers/WaitingHeartbeatScripts/Wait-TerminalState.ps1

function Wait-TerminalState {
    <#
    .SYNOPSIS
        Generic wait helper for "poll until terminal state" workflows with
        low-noise console animation.
    .DESCRIPTION
        - PollScript runs on schedule (PollSeconds) and returns an object (or
          $null if not found).
        - GetStatus extracts a status string from the object.
        - TerminalStates is a hashtable mapping status string -> terminal
          handling metadata.
 
        Interactive mode: - Animates a single-line spinner every TickMs (default
            250ms) - Hides console cursor while spinning - Clears spinner line
            before writing logs
 
        Non-interactive mode: - No spinner/cursor operations, just polls and
            logs on change
 
        By default, throws on timeout.
    .PARAMETER Target
        Friendly label for logs (search name, action identity, computer name,
        etc.)
    .PARAMETER PollScript
        Scriptblock that returns the current object (or $null). Called on the
        poll schedule.
    .PARAMETER GetStatus
        Scriptblock that takes the object and returns a status string.
    .PARAMETER TerminalStates
        Hashtable mapping status => @{ Level='Ok|Warn|Error|Info';
        Message='...'; Return=$true }
        - Message can be a string OR a scriptblock: { param($obj,$status) "..."
          }
    .PARAMETER TimeoutSeconds
        Total time to wait before timing out.
    .PARAMETER PollSeconds
        How often to poll the backing system.
    .PARAMETER TickMs
        Spinner refresh interval in milliseconds (interactive only).
    .PARAMETER Frames
        Spinner frames (defaults to braille spinner).
    .PARAMETER NotFoundToken
        Special internal token used when PollScript returns $null.
    .PARAMETER NotFoundMessage
        Log line emitted when transitioning into "not found" phase.
    .PARAMETER WaitingMessage
        Prefix for the animated waiting line.
    .PARAMETER HeartbeatSeconds
        If > 0, emit a "still waiting" log every N seconds even if status hasn't
        changed.
    .PARAMETER OnStatusChange
        Optional callback invoked when status changes. Useful for custom
        logs/telemetry.
    .PARAMETER ThrowOnTimeout
        If set (default), throws on timeout; otherwise returns $null or the last
        object (see ReturnLastOnTimeout).
    .PARAMETER ReturnLastOnTimeout
        If set, returns last seen object on timeout instead of $null (when
        ThrowOnTimeout is false).
    .PARAMETER ContextFormatter
        Scriptblock to build timeout context string: {
        param($lastObj,$lastStatus) "..." }
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Target,

        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [scriptblock]$PollScript,

        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [scriptblock]$GetStatus,

        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [hashtable]$TerminalStates,

        [ValidateRange(1, 86400)]
        [int]$TimeoutSeconds = 2400,

        [ValidateRange(1, 3600)]
        [int]$PollSeconds = 20,

        [ValidateRange(50, 2000)]
        [int]$TickMs = 100,

        [string[]]$frames = @(
            '|',
            '/',
            '-',
            '\'
        ),

        [string]$NotFoundToken = '<notfound>',
        [string]$NotFoundMessage = 'Not found yet...',
        [string]$WaitingMessage = 'Waiting ',

        [ValidateRange(0, 3600)]
        [int]$HeartbeatSeconds = 120,

        [scriptblock]$OnStatusChange,

        [switch]$ThrowOnTimeout = $true,
        [switch]$ReturnLastOnTimeout,

        [scriptblock]$ContextFormatter = { param($lastObj, $lastStatus) "LastSeenStatus=$lastStatus" }
    )

    # --- UI helpers ---
    $interactive = $Host.UI -and $Host.UI.RawUI -and -not [Console]::IsOutputRedirected
    $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
    $nextPoll = Get-Date
    $nextBeat = if ($HeartbeatSeconds -gt 0) { (Get-Date).AddSeconds($HeartbeatSeconds) } else { [datetime]::MaxValue }

    $lastStatus = $null
    $lastObj = $null
    $spin = 0

    # Cursor restore safety
    $cursorHidden = $false
    $cursorWasVisible = $true

    function _HideCursor {
        if (-not $interactive) { return $false }
        try {
            $script:cursorWasVisible = [Console]::CursorVisible
            [Console]::CursorVisible = $false
            return $true
        }
        catch { return $false }
    }

    function _ShowCursor {
        if (-not $interactive) { return }
        try { [Console]::CursorVisible = [bool]$script:cursorWasVisible } catch { }
    }

    function _ClearLine {
        if (-not $interactive) { return }
        # Clear generous width and return carriage
        Write-Host -NoNewline ("`r" + (' ' * 140) + "`r")
    }

    function _WriteWaitLine([string]$text) {
        if (-not $interactive) { return }
        Write-Host -NoNewline ("`r{0}" -f $text)
    }

    function _EmitLog([string]$level, [string]$message) {
        # Assumes you have Write-Log; replace if needed
        Write-Log -Level $level -Message $message
    }

    try {
        if (-not $interactive) {
            # --- Non-interactive: poll loop only ---
            while ((Get-Date) -lt $deadline) {
                $obj = $null
                try { $obj = & $PollScript } catch { $obj = $null }

                $status = $NotFoundToken
                if ($null -ne $obj) {
                    $lastObj = $obj
                    try { $status = & $GetStatus $obj } catch { $status = $NotFoundToken }
                }

                if ($status -ne $lastStatus) {
                    if ($status -eq $NotFoundToken) {
                        _EmitLog 'Info' ("{0}: {1}" -f $Target, $NotFoundMessage)
                    }
                    else {
                        _EmitLog 'Info' ("{0}: Status={1}" -f $Target, $status)
                    }

                    if ($OnStatusChange) { & $OnStatusChange $obj $status $lastStatus }
                    $lastStatus = $status
                }
                elseif ((Get-Date) -ge $nextBeat) {
                    _EmitLog 'Info' ("{0}: Still waiting (Status={1})..." -f $Target, $status)
                    $nextBeat = (Get-Date).AddSeconds($HeartbeatSeconds)
                }

                if ($TerminalStates.ContainsKey($status)) {
                    $meta = $TerminalStates[$status]
                    $lvl = $meta.Level
                    $msg = if ($meta.Message -is [scriptblock]) { & $meta.Message $obj $status } else { [string]$meta.Message }
                    if ($msg) { _EmitLog $lvl $msg }
                    return $obj
                }

                Start-Sleep -Seconds $PollSeconds
            }
        }
        else {
            # --- Interactive: tick loop with scheduled polls ---
            while ((Get-Date) -lt $deadline) {

                if (-not $cursorHidden) { $cursorHidden = _HideCursor }

                # Animate
                $frame = Get-DotPulse -Index $spin -Frames $Frames
                $spin++

                $now = Get-Date
                $secsToPoll = [Math]::Max(0, [int][Math]::Ceiling(($nextPoll - $now).TotalSeconds))

                # Poll when due
                if ($now -ge $nextPoll) {
                    $obj = $null
                    try { $obj = & $PollScript } catch { $obj = $null }

                    $status = $NotFoundToken
                    if ($null -ne $obj) {
                        $lastObj = $obj
                        try { $status = & $GetStatus $obj } catch { $status = $NotFoundToken }
                    }

                    if ($status -ne $lastStatus) {
                        _ClearLine
                        if ($status -eq $NotFoundToken) {
                            _EmitLog 'Info' ("{0}: {1}" -f $Target, $NotFoundMessage)
                        }
                        else {
                            _EmitLog 'Info' ("{0}: Status={1}" -f $Target, $status)
                        }

                        if ($OnStatusChange) { & $OnStatusChange $obj $status $lastStatus }
                        $lastStatus = $status
                        $spin = 0
                    }
                    elseif ($HeartbeatSeconds -gt 0 -and $now -ge $nextBeat) {
                        _ClearLine
                        _EmitLog 'Info' ("{0}: Still waiting (Status={1})..." -f $Target, $status)
                        $nextBeat = (Get-Date).AddSeconds($HeartbeatSeconds)
                    }

                    # Terminal?
                    if ($TerminalStates.ContainsKey($status)) {
                        $meta = $TerminalStates[$status]
                        $lvl = $meta.Level
                        $msg = if ($meta.Message -is [scriptblock]) { & $meta.Message $obj $status } else { [string]$meta.Message }

                        _ClearLine
                        Write-Host ""  # newline after spinner line
                        if ($msg) { _EmitLog $lvl $msg }
                        return $obj
                    }

                    # Next poll schedule
                    $nextPoll = (Get-Date).AddSeconds($PollSeconds)
                }

                # Update wait line (every TickMs)
                $phase = if ($lastStatus -eq $NotFoundToken) { 'not found' } elseif ($lastStatus) { $lastStatus } else { 'starting' }
                _WriteWaitLine ("{0}{1} ({2}, next poll in {3}s) " -f $WaitingMessage, $frame, $phase, $secsToPoll)

                Start-Sleep -Milliseconds $TickMs
            }
        }

        # Timeout
        $ctx = & $ContextFormatter $lastObj $lastStatus
        $msg = "Timed out waiting for terminal state. Target='$Target'. $ctx"

        if ($ThrowOnTimeout) {
            throw $msg
        }

        _ClearLine
        _EmitLog 'Warn' $msg
        if ($ReturnLastOnTimeout -and $lastObj) { return $lastObj }
        return $null
    }
    finally {
        if ($cursorHidden) { _ShowCursor }
        _ClearLine
    }
}

# SIG # Begin signature block
# MIIfAgYJKoZIhvcNAQcCoIIe8zCCHu8CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD2X7204okUs8Mz
# BWpTI+6XiOvtkQF+VSNz1qLP0jdONqCCGEowggUMMIIC9KADAgECAhAR+U4xG7FH
# qkyqS9NIt7l5MA0GCSqGSIb3DQEBCwUAMB4xHDAaBgNVBAMME1ZBRFRFSyBDb2Rl
# IFNpZ25pbmcwHhcNMjUxMjE5MTk1NDIxWhcNMjYxMjE5MjAwNDIxWjAeMRwwGgYD
# VQQDDBNWQURURUsgQ29kZSBTaWduaW5nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
# MIICCgKCAgEA3pzzZIUEY92GDldMWuzvbLeivHOuMupgpwbezoG5v90KeuN03S5d
# nM/eom/PcIz08+fGZF04ueuCS6b48q1qFnylwg/C/TkcVRo0WFcKoFGT8yGxdfXi
# caHtapZfbSRh73r7qR7w0CioVveNBVgfMsTgE0WKcuwxemvIe/ptmkfzwAiw/IAC
# Ib0E0BjiX4PySbwWy/QKy/qMXYY19xpRItVTKNBtXzADUtzPzUcFqJU83vM2gZFs
# Or0MhPvM7xEVkOWZFBAWAubbMCJ3rmwyVv9keVDJChhCeLSz2XR11VGDOEA2OO90
# Y30WfY9aOI2sCfQcKMeJ9ypkHl0xORdhUwZ3Wz48d3yJDXGkduPm2vl05RvnA4T6
# 29HVZTmMdvP2475/8nLxCte9IB7TobAOGl6P1NuwplAMKM8qyZh62Br23vcx1fXZ
# TJlKCxBFx1nTa6VlIJk+UbM4ZPm954peB/fIqEacm8LkZ0cPwmLE5ckW7hfK4Trs
# o+RaudU1sKeA+FvpOWgsPccVRWcEYyGkwbyTB3xrIBXA+YckbANZ0XL7fv7x29hn
# gXbZipGu3DnTISiFB43V4MhNDKZYfbWdxze0SwLe8KzIaKnwlwRgvXDMwXgk99Mi
# EbYa3DvA/5ZWikLW9PxBFD7Vdr8ZiG/tRC9I2Y6fnb+PVoZKc/2xsW0CAwEAAaNG
# MEQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQW
# BBRfYLVE8caSc990rnrIHUjoB7X/KjANBgkqhkiG9w0BAQsFAAOCAgEAiGB2Wmk3
# QBtd1LcynmxHzmu+X4Y5DIpMMNC2ahsqZtPUVcGqmb5IFbVuAdQphL6PSrDjaAR8
# 1S8uTfUnMa119LmIb7di7TlH2F5K3530h5x8JMj5EErl0xmZyJtSg7BTiBA/UrMz
# 6WCf8wWIG2/4NbV6aAyFwIojfAcKoO8ng44Dal/oLGzLO3FDE5AWhcda/FbqVjSJ
# 1zMfiW8odd4LgbmoyEI024KkwOkkPyJQ2Ugn6HMqlFLazAmBBpyS7wxdaAGrl18n
# 6bS7QuAwCd9hitdMMitG8YyWL6tKeRSbuTP5E+ASbu0Ga8/fxRO5ZSQhO6/5ro1j
# PGe1/Kr49Uyuf9VSCZdNIZAyjjeVAoxmV0IfxQLKz6VOG0kGDYkFGskvllIpQbQg
# WLuPLJxoskJsoJllk7MjZJwrpr08+3FQnLkRuisjDOc3l4VxFUsUe4fnJhMUONXT
# Sk7vdspgxirNbLmXU4yYWdsizz3nMUR0zebUW29A+HYme16hzrMPOeyoQjy4I5XX
# 3wXAFdworfPEr/ozDFrdXKgbLwZopymKbBwv6wtT7+1zVhJXr+jGVQ1TWr6R+8ea
# tIOFnY7HqGaxe5XB7HzOwJKdj+bpHAfXft1vUoiKr16VajLigcYCG8MdwC3sngO3
# JDyv2V+YMfsYBmItMGBwvizlQ6557NbK95EwggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqG
# SIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkx
# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4
# RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYg
# MjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphB
# cr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6p
# vF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHe
# HYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEd
# gkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjU
# jsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bR
# VFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeS
# LsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIV
# NSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL
# 6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2Zd
# SoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFU
# eEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/
# BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0j
# BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0
# cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E
# PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz
# dGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEw
# DQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/
# T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQ
# E7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9r
# EVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y
# 1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gx
# dEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3t
# y9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcy
# tL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEB
# YTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud
# /v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiS
# uEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZP
# ubdcMIIG7TCCBNWgAwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsF
# ADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNV
# BAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hB
# MjU2IDIwMjUgQ0ExMB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzEL
# MAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJE
# aWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUg
# MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMr
# V7pvUf+GcAoB38o3zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8
# dE2/pPvOx/Vj8TchTySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7M
# rxVyfQO9sMx6ZAWjFDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZ
# ZREr4h/GI6Dxb2UoyrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFO
# nHoRh6+86Ltc5zjPKHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+n
# igNJFmt6LAHvH3KSuNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeIt
# K/DhKbPxTTuGoX7wJNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1
# zBp+xUIZkpSFA8vWdoUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk
# 8iyyizNDIXj//cOgrY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsW
# eupWs7NpChUk555K096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAk
# prxMiXAJQ1XCmnCfgPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0G
# A1UdDgQWBBTkO/zyMe39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQG
# fHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYB
# BQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz
# cC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2lj
# ZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEy
# NTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hB
# MjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcB
# MA0GCSqGSIb3DQEBCwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWL
# pQq1b4URGnwWBdEZD9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgj
# g8K8elC4+oWCqnU/ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3Q
# YIUP2S3HQvHG1FDu+WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5
# bdrPbF6MRYs03h4obEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUG
# tMTaiLR9wjxUxu2hECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNE
# suEB7O7/cuvTQasnM9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6U
# Arb+BOVAkg2oOvol/DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG
# 0LIhp6GvReQGgMgYxQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWV
# FjF7mcr4C34Mj3ocCVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5
# t2nGj/ULLi49xTcBZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjs
# arfNZzGCBg4wggYKAgEBMDIwHjEcMBoGA1UEAwwTVkFEVEVLIENvZGUgU2lnbmlu
# ZwIQEflOMRuxR6pMqkvTSLe5eTANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3
# AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisG
# AQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCBcAuvzXtRl
# gCbMNmF4ngHC3sY8BNe3ydGhuIOBQTeD/DANBgkqhkiG9w0BAQEFAASCAgBLIaLo
# 0EGTrIVGC9wgi37XAtWCUQA0hhJmLxlkpQXnXQK0hawPwkjARoKHofNIhBiAgfnr
# A5mRpmyy/vA5sbcHRpNdwUIgeaUdTiLa21k6lh3JlvywNR5IT6UzX1RMAIwWsmWR
# 6rQT99Jpa2nvUbPzGr6Pice7QdAAvqU2Hd0I8BdrdGwcJ2MbQlABqVfZ1zsi1bwV
# Lgki5oZdb8td3HkFrJRrPwEHtBWG/kNqKUMLwO+3t4fJ+l/ZA4Zfpi0YYaMc40ZL
# 411oxdSK2PScTwbyGsu/pH82akVEs3DndcL1UOrYWnTpTwPtMInytNgNsXvQ5C+p
# 5eSql4b8CwpPDMeLuJlazrSlEncpGCDrtHS0ecQdJmw2cnX9jxtbyhMzpiHgIMVc
# eywwXAzf5k7pP7/O34LQen5L/EnsTh0Y4Kcs5eNIcqaO0SjEx42VmcjXv4ibC2tS
# lsWrwDfgDKx7xP9ojwxwKFrwPm0kup6mEHY1m9xUKIqUrFCtY9vVmy/1IH9dLEHx
# EJOAEnzbuAsYk6Ndw/3uk7fUI0MLuUOoi0H+voSe0r6EGtZ8clzD7E/+cuok3BHi
# C6bQ8wMIHxQJrAvjk85JgOHJpXoiQCMRrwEcaH/ejOEcw0EgCW5xq4drR0m+O2RQ
# FDZH2vqtZLldabdu1RSlitZA+LjE/xRTNIa0y6GCAyYwggMiBgkqhkiG9w0BCQYx
# ggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg
# SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcg
# UlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeVdGgwDQYJYIZI
# AWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJ
# BTEPFw0yNjA2MDkyMzIwMTNaMC8GCSqGSIb3DQEJBDEiBCDpvlsbgRCbD12puchM
# jME/p+9fl4tvGCOemQ1OHfhMYzANBgkqhkiG9w0BAQEFAASCAgAyz53LFfSSNbEJ
# bzHpzCfe4I2GgarCx8Migd4ng6h7ZC1Q5Am/vE7PbMXbbbm5a7a2MpoV1CqlI9db
# oDRKGAsuZgC/xzhRCsyGX789aHuubrSx5yu23kG+32iq73aVDX1+eZZhJQBiCai2
# aix/mEx/gwtOPpUPx1QI3ow8GOL+xaLy06gFTDL+Mp4cYTZYrIlZ29vkfoazxbFb
# aNka7RQA9kULs7wy6S/h/0KKuOw1SXhiW36g4+Q3+WS+AEyCFo4iWMLwuiwe9DUW
# 8cJwoJDbM43Lo37VHwure6fDAZB7JdY9J2Czy0XL30LHuPqiRD/QuX9RNbOG/6z9
# 0i318SOPNYqgFml3isxKiQwwkCvp+g+yysJcn4CdG8nsu3fALFztu9s+RZ1CzrjM
# onfyy8MfY9r15+nLZ3oAWJNdENaNAnS1CvbRP2kepuwW9WKlYuH6V312TCHa1i7f
# NQMbfGJIqeYtAQ8cSDxD8szP+XplXHXc3LBt4DXS7rArN+UzNbJuGmLxiDWG4J6f
# 6sxdyuUR4TCdTmY2/hUi7/+0xTRMXV/07HG27Ub8EvPi4/T09ox5fvUQ5xplakUY
# PijgSgbhIzP70QEi/aAE5yNE0MYsWH8IFdHr8YsYKa+aqQu/jUkibEXk2MIoXtRw
# 9MXH83Ucevcsb/JggJ1SALrTbFEuTg==
# SIG # End signature block