helpers/test/Invoke-WUGAllE2ETests.ps1

<#
.SYNOPSIS
    Master E2E test runner for all WhatsUpGoldPS test suites.
 
.DESCRIPTION
    Runs every E2E test suite, captures pass/fail results with timing,
    categorises them as "Core Module" or "Helpers", and produces a unified
    HTML dashboard using Test-Dashboard-Template.html.
 
    Test suites:
      Core Module
        - Module Cmdlets (Invoke-WUGModuleTest.ps1)
        - Credential API (Invoke-WUGCredentialE2ETest.ps1)
 
      Helpers
        - Discovery Framework (Invoke-WUGDiscoveryHelperTest.ps1)
        - Discovery Providers (Invoke-WUGDiscoveryE2ETest.ps1)
        - Geolocation (Invoke-WUGGeomapTest.ps1)
 
    WUG server credentials are resolved from the DPAPI vault
    (WUG.Server or WUG.192.168.74.74). When no vault entry exists
    and -NonInteractive is set, suites that require a WUG connection
    are skipped automatically.
 
.PARAMETER Category
    Run only the specified category. Default: both.
 
.PARAMETER IncludeSuite
    Run only the named suite(s). Tab-completes to all known names.
 
.PARAMETER ExcludeSuite
    Skip the named suite(s).
 
.PARAMETER NonInteractive
    Skip any suite that would require interactive prompts.
 
.PARAMETER OpenReport
    Open the unified HTML dashboard in the default browser.
 
.PARAMETER OutputPath
    Directory for the unified report. Default: $env:TEMP\AllE2ETests.
 
.EXAMPLE
    .\Invoke-WUGAllE2ETests.ps1 -NonInteractive -OpenReport
 
.EXAMPLE
    .\Invoke-WUGAllE2ETests.ps1 -Category Helpers -OpenReport
 
.EXAMPLE
    .\Invoke-WUGAllE2ETests.ps1 -IncludeSuite 'Discovery Framework','Discovery Providers'
 
.NOTES
    Author : jason@wug.ninja
    Created : 2026-04-19
    Requires: PowerShell 5.1+, Windows (DPAPI vault), WhatsUpGoldPS module
#>

[CmdletBinding()]
param(
    [ValidateSet('All', 'Core Module', 'Helpers')]
    [string]$Category = 'All',

    [ValidateSet('Module Cmdlets', 'Credential API', 'Discovery Framework', 'Discovery Providers', 'Geolocation')]
    [string[]]$IncludeSuite,

    [ValidateSet('Module Cmdlets', 'Credential API', 'Discovery Framework', 'Discovery Providers', 'Geolocation')]
    [string[]]$ExcludeSuite,

    [switch]$NonInteractive,
    [switch]$OpenReport,
    [string]$OutputPath
)

# ============================================================================
#region Setup
# ============================================================================

$ErrorActionPreference = 'Continue'
$scriptDir   = Split-Path $MyInvocation.MyCommand.Path -Parent
$timestamp   = Get-Date -Format 'yyyyMMdd-HHmmss'

if (-not $OutputPath) { $OutputPath = Join-Path $env:TEMP 'AllE2ETests' }
if (-not (Test-Path $OutputPath)) { New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null }

$masterResults = [System.Collections.Generic.List[PSCustomObject]]::new()
$suiteTimings  = [System.Collections.Generic.List[PSCustomObject]]::new()

# ============================================================================
#region Load dependencies
# ============================================================================

$discoveryHelpersPath = Join-Path $scriptDir '..\discovery\DiscoveryHelpers.ps1'
if (Test-Path $discoveryHelpersPath) {
    . $discoveryHelpersPath
    Write-Host " Loaded DiscoveryHelpers.ps1" -ForegroundColor Green
}

# Load module for core tests
$modulePath = Join-Path $scriptDir '..\..\WhatsUpGoldPS.psd1'
if (Test-Path $modulePath) {
    Import-Module $modulePath -Force -ErrorAction SilentlyContinue
}

# ============================================================================
#region Resolve WUG server credentials from vault
# ============================================================================

$wugServer = $null
$wugCred   = $null

$vaultNames = @('WUG.Server', 'WUG.192.168.74.74')
foreach ($vn in $vaultNames) {
    try {
        $vaultData = Get-DiscoveryCredential -Name $vn -ErrorAction SilentlyContinue
        if ($vaultData) {
            $wugParts = "$vaultData" -split '\|'
            if ($wugParts.Count -ge 5) {
                $wugServer = $wugParts[0]
                $secPw     = ConvertTo-SecureString $wugParts[4] -AsPlainText -Force
                $wugCred   = [PSCredential]::new($wugParts[3], $secPw)
                $secPw     = $null
                Write-Host " WUG server loaded from vault: $vn ($wugServer)" -ForegroundColor Green
                break
            }
        }
    }
    catch { Write-Verbose "Vault '$vn' read failed: $_" }
}

$hasWUG = $null -ne $wugServer -and $null -ne $wugCred

if (-not $hasWUG -and -not $NonInteractive) {
    Write-Host " No WUG vault credential found." -ForegroundColor Yellow
    $wugServer = Read-Host " Enter WhatsUp Gold server hostname or IP (blank to skip WUG tests)"
    if ($wugServer) {
        $wugCred = Get-Credential -Message "Enter WhatsUp Gold credentials"
        $hasWUG  = $true
    }
}

# ============================================================================
#region Define test suites
# ============================================================================

$allSuites = @(
    # ---------- Core Module ----------
    @{
        Name        = 'Module Cmdlets'
        Category    = 'Core Module'
        Script      = 'Invoke-WUGModuleTest.ps1'
        RequiresWUG = $true
        BuildParams = {
            @{
                ServerUri       = $wugServer
                Credential      = $wugCred
                IgnoreSSLErrors = $true
            }
        }
        MapResult   = {
            param($r)
            [PSCustomObject]@{
                Test   = $r.Cmdlet
                Detail = $r.Endpoint
                Status = $r.Status
                Note   = $r.Detail
            }
        }
    }
    @{
        Name        = 'Credential API'
        Category    = 'Core Module'
        Script      = 'Invoke-WUGCredentialE2ETest.ps1'
        RequiresWUG = $true
        BuildParams = {
            $p = @{}
            if ($wugServer) { $p['WUGServer'] = $wugServer }
            if ($wugCred)   { $p['Credential'] = $wugCred }
            $p
        }
        MapResult   = {
            param($r)
            [PSCustomObject]@{
                Test   = $r.Name
                Detail = $r.Group
                Status = $r.Status
                Note   = $r.Detail
            }
        }
    }

    # ---------- Helpers ----------
    @{
        Name        = 'Discovery Framework'
        Category    = 'Helpers'
        Script      = 'Invoke-WUGDiscoveryHelperTest.ps1'
        RequiresWUG = $false
        BuildParams = { @{} }
        MapResult   = {
            param($r)
            [PSCustomObject]@{
                Test   = $r.Name
                Detail = $r.Group
                Status = $r.Status
                Note   = $r.Detail
            }
        }
    }
    @{
        Name        = 'Discovery Providers'
        Category    = 'Helpers'
        Script      = 'Invoke-WUGDiscoveryE2ETest.ps1'
        RequiresWUG = $false
        BuildParams = { @{ NonInteractive = $true } }
        MapResult   = {
            param($r)
            [PSCustomObject]@{
                Test       = $r.Cmdlet
                Detail     = $r.Endpoint
                Status     = $r.Status
                Note       = $r.Detail
                Duration   = $r.Duration
                DurationMs = $r.DurationMs
            }
        }
    }
    @{
        Name        = 'Geolocation'
        Category    = 'Helpers'
        Script      = 'Invoke-WUGGeomapTest.ps1'
        RequiresWUG = $true
        BuildParams = {
            $p = @{ IgnoreSSLErrors = $true }
            if ($wugServer) { $p['ServerUri']  = $wugServer }
            if ($wugCred)   { $p['Credential'] = $wugCred }
            $p
        }
        MapResult   = {
            param($r)
            [PSCustomObject]@{
                Test   = $r.Cmdlet
                Detail = $r.Endpoint
                Status = $r.Status
                Note   = $r.Detail
            }
        }
    }
)

# ============================================================================
#region Filter suites by category / include / exclude
# ============================================================================

$runnableSuites = $allSuites

if ($Category -ne 'All') {
    $runnableSuites = @($runnableSuites | Where-Object { $_.Category -eq $Category })
}
if ($IncludeSuite) {
    $runnableSuites = @($runnableSuites | Where-Object { $_.Name -in $IncludeSuite })
}
if ($ExcludeSuite) {
    $runnableSuites = @($runnableSuites | Where-Object { $_.Name -notin $ExcludeSuite })
}

# ============================================================================
#region Banner
# ============================================================================

$divider = '=' * 70
Write-Host ""
Write-Host $divider -ForegroundColor Cyan
Write-Host " WhatsUpGoldPS - Master E2E Test Runner" -ForegroundColor Cyan
Write-Host $divider -ForegroundColor Cyan
Write-Host " Suites : $($runnableSuites.Count)" -ForegroundColor White
Write-Host " WUG Server : $(if ($hasWUG) { $wugServer } else { '(none)' })" -ForegroundColor $(if ($hasWUG) { 'Green' } else { 'Yellow' })
Write-Host " Output : $OutputPath" -ForegroundColor White
Write-Host " Timestamp : $timestamp" -ForegroundColor White
Write-Host $divider -ForegroundColor Cyan
Write-Host ""

$overallSW = [System.Diagnostics.Stopwatch]::StartNew()

# ============================================================================
#region Run each suite
# ============================================================================

$suiteIndex = 0
foreach ($suite in $runnableSuites) {
    $suiteIndex++

    # --- Skip check ---
    if ($suite.RequiresWUG -and -not $hasWUG) {
        Write-Host " [$suiteIndex/$($runnableSuites.Count)] $($suite.Name)" -ForegroundColor White -NoNewline
        Write-Host " SKIPPED (no WUG server)" -ForegroundColor Yellow
        $suiteTimings.Add([PSCustomObject]@{
            Suite     = $suite.Name
            Category  = $suite.Category
            Status    = 'Skipped'
            Pass      = 0
            Fail      = 0
            Skip      = 1
            Total     = 0
            Duration  = '-'
            DurationMs = 0
        })
        $masterResults.Add([PSCustomObject]@{
            Category   = $suite.Category
            Suite      = $suite.Name
            Test       = '(suite skipped)'
            Detail     = 'No WUG server credentials available'
            Status     = 'Skipped'
            Duration   = '-'
            DurationMs = 0
            Note       = ''
        })
        continue
    }

    Write-Host ""
    Write-Host " [$suiteIndex/$($runnableSuites.Count)] $($suite.Category) / $($suite.Name)" -ForegroundColor Cyan
    Write-Host " $('-' * 50)" -ForegroundColor DarkCyan

    $scriptPath = Join-Path $scriptDir $suite.Script
    if (-not (Test-Path $scriptPath)) {
        Write-Host " Script not found: $scriptPath" -ForegroundColor Red
        $suiteTimings.Add([PSCustomObject]@{
            Suite      = $suite.Name
            Category   = $suite.Category
            Status     = 'Fail'
            Pass       = 0
            Fail       = 1
            Skip       = 0
            Total      = 0
            Duration   = '-'
            DurationMs = 0
        })
        continue
    }

    # Build parameters
    $params = & $suite.BuildParams

    # Run with timing
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $suiteError = $null
    $rawOutput  = $null

    try {
        $rawOutput = & $scriptPath @params 2>&1
    }
    catch {
        $suiteError = $_.Exception.Message
    }

    $sw.Stop()
    $elapsedMs = $sw.ElapsedMilliseconds
    if ($elapsedMs -ge 60000) {
        $elapsed = '{0:0}m {1:0}s' -f [Math]::Floor($elapsedMs / 60000), [Math]::Floor(($elapsedMs % 60000) / 1000)
    }
    elseif ($elapsedMs -ge 1000) {
        $elapsed = '{0:0.0}s' -f ($elapsedMs / 1000)
    }
    else {
        $elapsed = "${elapsedMs}ms"
    }

    # Extract PSCustomObjects with a Status property (test results)
    $suiteResults = @()
    if ($rawOutput) {
        $suiteResults = @($rawOutput | Where-Object {
            $_ -is [PSCustomObject] -and $_.PSObject.Properties['Status']
        })
    }

    if ($suiteError -and $suiteResults.Count -eq 0) {
        # Suite crashed
        Write-Host " CRASHED: $suiteError" -ForegroundColor Red
        $suiteTimings.Add([PSCustomObject]@{
            Suite      = $suite.Name
            Category   = $suite.Category
            Status     = 'Fail'
            Pass       = 0
            Fail       = 1
            Skip       = 0
            Total      = 0
            Duration   = $elapsed
            DurationMs = $elapsedMs
        })
        $masterResults.Add([PSCustomObject]@{
            Category   = $suite.Category
            Suite      = $suite.Name
            Test       = '(suite error)'
            Detail     = ''
            Status     = 'Fail'
            Duration   = $elapsed
            DurationMs = $elapsedMs
            Note       = $suiteError
        })
        continue
    }

    # Map results to common format
    $mapped = @()
    foreach ($r in $suiteResults) {
        $m = & $suite.MapResult $r
        $mapped += $m
    }

    # Calculate suite-level stats
    $sPass = @($mapped | Where-Object { $_.Status -eq 'Pass' }).Count
    $sFail = @($mapped | Where-Object { $_.Status -eq 'Fail' }).Count
    $sSkip = @($mapped | Where-Object { $_.Status -eq 'Skipped' }).Count
    $sTotal = $mapped.Count

    $suiteStatus = if ($sFail -gt 0) { 'Fail' } elseif ($sPass -gt 0) { 'Pass' } else { 'Skipped' }
    $statusColor = switch ($suiteStatus) { 'Pass' { 'Green' } 'Fail' { 'Red' } default { 'Yellow' } }

    Write-Host " $suiteStatus" -ForegroundColor $statusColor -NoNewline
    Write-Host " ($sPass pass, $sFail fail, $sSkip skip, $elapsed)" -ForegroundColor Gray

    $suiteTimings.Add([PSCustomObject]@{
        Suite      = $suite.Name
        Category   = $suite.Category
        Status     = $suiteStatus
        Pass       = $sPass
        Fail       = $sFail
        Skip       = $sSkip
        Total      = $sTotal
        Duration   = $elapsed
        DurationMs = $elapsedMs
    })

    # Add to master results with Category/Suite tags and suite-level timing
    $perTestMs = if ($sTotal -gt 0) { [Math]::Round($elapsedMs / $sTotal) } else { $elapsedMs }
    foreach ($m in $mapped) {
        $dur   = if ($m.PSObject.Properties['Duration']   -and $m.Duration)   { $m.Duration }   else { '' }
        $durMs = if ($m.PSObject.Properties['DurationMs'] -and $m.DurationMs) { $m.DurationMs } else { 0 }
        $masterResults.Add([PSCustomObject]@{
            Category   = $suite.Category
            Suite      = $suite.Name
            Test       = $m.Test
            Detail     = $m.Detail
            Status     = $m.Status
            Duration   = $dur
            DurationMs = $durMs
            Note       = $m.Note
        })
    }
}

$overallSW.Stop()
$overallMs = $overallSW.ElapsedMilliseconds
if ($overallMs -ge 60000) {
    $overallElapsed = '{0:0}m {1:0}s' -f [Math]::Floor($overallMs / 60000), [Math]::Floor(($overallMs % 60000) / 1000)
}
else {
    $overallElapsed = '{0:0.0}s' -f ($overallMs / 1000)
}

# ============================================================================
#region Console Summary
# ============================================================================

Write-Host ""
Write-Host $divider -ForegroundColor Cyan
Write-Host " MASTER RESULTS SUMMARY" -ForegroundColor Cyan
Write-Host $divider -ForegroundColor Cyan
Write-Host ""

# Per-suite summary
foreach ($st in $suiteTimings) {
    $icon  = switch ($st.Status) { 'Pass' { '[+]' } 'Fail' { '[-]' } 'Skipped' { '[ ]' } default { '[?]' } }
    $color = switch ($st.Status) { 'Pass' { 'Green' } 'Fail' { 'Red' } 'Skipped' { 'DarkGray' } default { 'Yellow' } }
    Write-Host " $icon " -ForegroundColor $color -NoNewline
    Write-Host ("{0,-14}" -f $st.Category) -ForegroundColor Gray -NoNewline
    Write-Host ("{0,-25}" -f $st.Suite) -ForegroundColor White -NoNewline
    Write-Host ("{0,4} pass " -f $st.Pass) -ForegroundColor Green -NoNewline
    Write-Host ("{0,3} fail " -f $st.Fail) -ForegroundColor $(if ($st.Fail -gt 0) { 'Red' } else { 'Green' }) -NoNewline
    Write-Host ("{0,3} skip " -f $st.Skip) -ForegroundColor Yellow -NoNewline
    Write-Host ("{0,10}" -f $st.Duration) -ForegroundColor Gray
}

Write-Host ""

$totalPass = ($masterResults | Where-Object { $_.Status -eq 'Pass' }).Count
$totalFail = ($masterResults | Where-Object { $_.Status -eq 'Fail' }).Count
$totalSkip = ($masterResults | Where-Object { $_.Status -eq 'Skipped' }).Count
$totalAll  = $masterResults.Count

$overallStatus = if ($totalFail -gt 0) { 'FAIL' } else { 'PASS' }
$overallColor  = if ($totalFail -gt 0) { 'Red' } else { 'Green' }

Write-Host " OVERALL: $overallStatus | Total: $totalAll | Pass: $totalPass | Fail: $totalFail | Skip: $totalSkip | Time: $overallElapsed" -ForegroundColor $overallColor
Write-Host ""

# Show failed tests
if ($totalFail -gt 0) {
    Write-Host " FAILED TESTS:" -ForegroundColor Red
    $masterResults | Where-Object { $_.Status -eq 'Fail' } | ForEach-Object {
        Write-Host " [$($_.Suite)] $($_.Test): $($_.Note)" -ForegroundColor Red
    }
    Write-Host ""
}

# ============================================================================
#region Generate unified HTML dashboard
# ============================================================================

# Build data JSON
$dataRows = @($masterResults | Select-Object Category, Suite, Test, Detail, Status, Duration, DurationMs, Note)
$dataJson = ConvertTo-Json -InputObject $dataRows -Depth 5 -Compress
if ($masterResults.Count -eq 1) { $dataJson = "[$dataJson]" }

# Build suite timings JSON
$timingsData = @($suiteTimings | Select-Object Suite, Category, Status, Pass, Fail, Skip, Total, Duration, DurationMs)
$timingsJson = ConvertTo-Json -InputObject $timingsData -Depth 5 -Compress
if ($suiteTimings.Count -eq 1) { $timingsJson = "[$timingsJson]" }

# Build failed tests panel HTML
$failedPanelHtml = ''
$failedRows = @($masterResults | Where-Object { $_.Status -eq 'Fail' })
if ($failedRows.Count -gt 0) {
    $failedPanelHtml = @"
    <div class="card border-danger mb-3" id="failedPanel">
      <div class="card-header bg-danger text-white d-flex justify-content-between align-items-center" data-bs-toggle="collapse" data-bs-target="#failedBody" role="button">
        <span><i class="bi bi-exclamation-triangle-fill me-2"></i>Failed Tests ($($failedRows.Count))</span>
        <i class="bi bi-chevron-down" id="failedChevron"></i>
      </div>
      <div class="collapse show" id="failedBody">
        <div class="list-group list-group-flush">
"@

    foreach ($fr in $failedRows) {
        $escapedNote = [System.Web.HttpUtility]::HtmlEncode($fr.Note)
        $escapedTest = [System.Web.HttpUtility]::HtmlEncode($fr.Test)
        $escapedDetail = [System.Web.HttpUtility]::HtmlEncode($fr.Detail)
        $failedPanelHtml += @"
          <div class="list-group-item list-group-item-danger">
            <div class="d-flex justify-content-between align-items-start">
              <div>
                <span class="badge bg-secondary me-1">$([System.Web.HttpUtility]::HtmlEncode($fr.Suite))</span>
                <strong>$escapedTest</strong>
                <small class="text-muted ms-2">$escapedDetail</small>
              </div>
            </div>
            <div class="mt-1"><code class="text-danger">$escapedNote</code></div>
          </div>
"@

    }
    $failedPanelHtml += @"
        </div>
      </div>
    </div>
"@

}

$reportTitle = "WhatsUpGoldPS Master E2E Results - $timestamp"
$updateTime  = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')

$html = @"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
    <title>$reportTitle</title>
    <link rel="icon" type="image/x-icon" href="https://wug.ninja/favicon.ico">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table/dist/bootstrap-table.min.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.min.css">
    <script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core/dist/umd/popper.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap-table/dist/bootstrap-table.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/file-saver/dist/FileSaver.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/xlsx/dist/xlsx.full.min.js"></script>
    <style>
        body { background-color: #1a1a2e; color: #e0e0e0; font-family: 'Segoe UI', system-ui, sans-serif; }
        .summary-card { border-left: 4px solid; cursor: pointer; transition: box-shadow .15s, transform .1s; user-select: none; min-width: 90px; background: #16213e; border-radius: 8px; }
        .summary-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,.3); transform: translateY(-2px); }
        .summary-card.active-filter { box-shadow: 0 0 0 2px #4cc9f0; }
        .card-total { border-left-color: #a0a0b0; }
        .card-green { border-left-color: #06d6a0; }
        .card-red { border-left-color: #ef476f; }
        .card-orange { border-left-color: #ffd166; }
        .summary-card .text-muted { color: #8888aa !important; }
        .summary-card h4 { margin-bottom: 0; }
        .val-total { color: #c0c0d0; }
        .val-green { color: #06d6a0; font-weight: 700; }
        .val-red { color: #ef476f; font-weight: 700; }
        .val-orange { color: #ffd166; font-weight: 700; }
 
        .card { background: #16213e; border-color: #2a2a4a; }
        .card-header { border-bottom-color: #2a2a4a; }
 
        /* Suite table */
        .suite-table { width: 100%; }
        .suite-table th { background: #0f3460; color: #e0e0e0; padding: 8px 12px; font-weight: 600; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.5px; }
        .suite-table td { padding: 8px 12px; border-top: 1px solid #2a2a4a; color: #e0e0e0; }
        .suite-row { cursor: pointer; transition: background .15s; }
        .suite-row:hover { background: #1a3a6a !important; }
        .suite-row.active { background: #1a3a6a !important; box-shadow: inset 3px 0 0 #4cc9f0; }
        .suite-row td:first-child { border-left: 3px solid transparent; }
        .suite-row.active td:first-child { border-left-color: #4cc9f0; }
 
        /* Badges */
        .badge-pass { background: #06d6a0; color: #1a1a2e; }
        .badge-fail { background: #ef476f; color: #fff; }
        .badge-skip { background: #ffd166; color: #1a1a2e; }
        .badge-unknown { background: #6c757d; }
 
        /* Failed panel */
        #failedPanel { border-color: #ef476f !important; }
        #failedPanel .card-header { background: #ef476f !important; }
        #failedPanel .list-group-item { background: #2a1525; border-color: #3a2535; color: #e0e0e0; }
        #failedPanel code { color: #ff8fa3 !important; background: none; font-size: 0.9rem; }
 
        /* Bootstrap-table dark overrides */
        .table { color: #e0e0e0; --bs-table-bg: #16213e; --bs-table-striped-bg: #1a2744; --bs-table-hover-bg: #1a3a6a; --bs-table-border-color: #2a2a4a; }
        .table thead th { background: #0f3460 !important; color: #e0e0e0; border-color: #2a2a4a; }
        .fixed-table-toolbar .search input { background: #16213e; color: #e0e0e0; border-color: #2a2a4a; }
        .fixed-table-toolbar .btn { background: #0f3460; color: #e0e0e0; border-color: #2a2a4a; }
        .fixed-table-toolbar .btn:hover { background: #1a3a6a; }
        .fixed-table-pagination .page-link { background: #16213e; color: #e0e0e0; border-color: #2a2a4a; }
        .fixed-table-pagination .page-item.active .page-link { background: #4cc9f0; border-color: #4cc9f0; color: #1a1a2e; }
        .dropdown-menu { background: #16213e; border-color: #2a2a4a; }
        .dropdown-item { color: #e0e0e0; }
        .dropdown-item:hover { background: #1a3a6a; color: #fff; }
        caption { background: #0f3460 !important; }
        caption h6 { color: #e0e0e0 !important; }
 
        .status-green { color: #06d6a0; font-weight: 600; }
        .status-red { color: #ef476f; font-weight: 600; }
        .status-orange { color: #ffd166; font-weight: 600; }
        .status-grey { color: #8888aa; font-weight: 600; }
        .status-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 6px; }
        .dot-green { background-color: #06d6a0; }
        .dot-red { background-color: #ef476f; }
        .dot-orange { background-color: #ffd166; }
        .dot-grey { background-color: #8888aa; }
 
        .note-cell { max-width: 400px; word-break: break-word; }
        .note-fail { color: #ff8fa3; font-family: 'Cascadia Code', 'Fira Code', monospace; font-size: 0.85rem; }
 
        .row-counter { font-size: 0.85rem; color: #8888aa; padding: 6px 0; white-space: nowrap; }
        .fixed-table-toolbar { display: flex; align-items: center; gap: 8px; padding: 4px 0; }
        .fixed-table-toolbar > .row-counter { flex: 1; order: 1; }
        .fixed-table-toolbar > .search { order: 2; }
        .fixed-table-toolbar > .columns { order: 3; }
 
        .progress { background: #2a2a4a; height: 6px; border-radius: 3px; }
        .progress-bar-pass { background: #06d6a0; }
        .progress-bar-fail { background: #ef476f; }
        .progress-bar-skip { background: #ffd166; }
    </style>
</head>
<body>
<div class="container-fluid p-3">
    <!-- Header -->
    <div class="d-flex justify-content-between align-items-center mb-3">
        <h5 class="mb-0" style="color:#4cc9f0"><i class="bi bi-clipboard2-pulse me-2"></i>$reportTitle</h5>
        <small class="text-muted">$updateTime</small>
    </div>
 
    <!-- Summary Cards -->
    <div class="row mb-3 g-2">
        <div class="col" id="filterTotal" style="max-width:16%">
            <div class="card summary-card card-total h-100"><div class="card-body py-2 px-3">
                <div class="text-muted small">Total Tests</div><h4 id="totalTests" class="mb-0 val-total">0</h4>
            </div></div>
        </div>
        <div class="col" id="filterPass" style="max-width:16%">
            <div class="card summary-card card-green h-100"><div class="card-body py-2 px-3">
                <div class="text-muted small">Passed</div><h4 id="passTests" class="mb-0 val-green">0</h4>
            </div></div>
        </div>
        <div class="col" id="filterFail" style="max-width:16%">
            <div class="card summary-card card-red h-100"><div class="card-body py-2 px-3">
                <div class="text-muted small">Failed</div><h4 id="failTests" class="mb-0 val-red">0</h4>
            </div></div>
        </div>
        <div class="col" id="filterSkipped" style="max-width:16%">
            <div class="card summary-card card-orange h-100"><div class="card-body py-2 px-3">
                <div class="text-muted small">Skipped</div><h4 id="skipTests" class="mb-0 val-orange">0</h4>
            </div></div>
        </div>
    </div>
 
    <!-- Failed Tests Panel -->
    $failedPanelHtml
 
    <!-- Suite Timing Summary -->
    <div class="card mb-3">
      <div class="card-header bg-dark text-white d-flex justify-content-between align-items-center" data-bs-toggle="collapse" data-bs-target="#suiteSummary" role="button">
        <span><i class="bi bi-speedometer2 me-2"></i>Suite Timing Summary (Total: $overallElapsed) <small class="ms-2 text-muted">click a row to filter</small></span>
        <i class="bi bi-chevron-down" id="suiteChevron"></i>
      </div>
      <div class="collapse show" id="suiteSummary">
        <div class="card-body p-0">
          <table class="suite-table" id="suiteTable">
            <thead><tr><th>Category</th><th>Suite</th><th>Pass</th><th>Fail</th><th>Skip</th><th>Total</th><th style="width:120px">Progress</th><th>Duration</th><th>Status</th></tr></thead>
            <tbody id="suiteTableBody"></tbody>
          </table>
        </div>
      </div>
    </div>
 
    <!-- Detail Table -->
    <div class="card">
      <div class="card-header d-flex justify-content-between align-items-center" style="background:#0f3460">
        <span class="text-white"><i class="bi bi-table me-2"></i>All Test Results <span id="suiteFilterLabel" class="badge bg-info ms-2" style="display:none"></span></span>
        <button class="btn btn-sm btn-outline-light" id="clearSuiteFilter" style="display:none"><i class="bi bi-x-lg me-1"></i>Clear filter</button>
      </div>
      <table id="table" data-classes="table table-striped table-bordered table-hover table-sm caption-top"
          data-show-toggle="true" data-show-columns="true" data-search="true"
          data-pagination="true" data-page-size="50" data-page-list="[25, 50, 100, 200, All]"
          data-sort-name="Status" data-sort-order="asc">
      </table>
    </div>
</div>
 
<script type="text/javascript">
var `$table = `$('#table'), allData = [], activeFilter = null, activeSuiteFilter = null;
var suiteTimings = $timingsJson;
 
function escapeHtml(t) { if(t==null)return''; return String(t).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#039;'); }
 
function formatTestStatus(value, row) {
    if (!value) return '<span class="status-grey"><span class="status-dot dot-grey"></span>Unknown</span>';
    var v = String(value).toLowerCase();
    if (v==='pass'||v==='passed') return '<span class="status-green"><span class="status-dot dot-green"></span>' + escapeHtml(value) + '</span>';
    if (v==='fail'||v==='failed') return '<span class="status-red"><span class="status-dot dot-red"></span>' + escapeHtml(value) + '</span>';
    if (v==='skipped'||v==='skip') return '<span class="status-orange"><span class="status-dot dot-orange"></span>' + escapeHtml(value) + '</span>';
    return '<span class="status-grey"><span class="status-dot dot-grey"></span>' + escapeHtml(value) + '</span>';
}
 
function formatNote(value, row) {
    if (!value) return '';
    var s = String(row.Status||'').toLowerCase();
    if (s==='fail'||s==='failed') return '<span class="note-fail">' + escapeHtml(value) + '</span>';
    return '<span class="note-cell">' + escapeHtml(value) + '</span>';
}
 
function durationSorter(a, b, rowA, rowB) {
    var msA = (rowA && rowA.DurationMs) ? Number(rowA.DurationMs) : 0;
    var msB = (rowB && rowB.DurationMs) ? Number(rowB.DurationMs) : 0;
    return msA - msB;
}
 
function computeSummary(data) {
    var pass=0, fail=0, skip=0;
    data.forEach(function(r) {
        var s = String(r.Status||'').toLowerCase();
        if (s==='pass'||s==='passed') pass++; else if (s==='fail'||s==='failed') fail++; else if (s==='skipped'||s==='skip') skip++;
    });
    `$('#totalTests').text(data.length); `$('#passTests').text(pass); `$('#failTests').text(fail); `$('#skipTests').text(skip);
}
 
function getFilteredData() {
    var data = allData;
    if (activeSuiteFilter) {
        data = data.filter(function(r) { return r.Suite === activeSuiteFilter; });
    }
    if (activeFilter) {
        data = data.filter(function(r) {
            var s = String(r.Status||'').toLowerCase();
            switch(activeFilter) { case 'pass': return s==='pass'||s==='passed'; case 'fail': return s==='fail'||s==='failed'; case 'skipped': return s==='skipped'||s==='skip'; default: return true; }
        });
    }
    return data;
}
 
function applyFilters() {
    var data = getFilteredData();
    `$table.bootstrapTable('load', data);
}
 
function toggleStatusFilter(filterName) {
    if (activeFilter === filterName) { activeFilter = null; `$('.summary-card').removeClass('active-filter'); }
    else { activeFilter = filterName; `$('.summary-card').removeClass('active-filter'); `$('#filter' + filterName.charAt(0).toUpperCase() + filterName.slice(1) + ' .summary-card').addClass('active-filter'); }
    applyFilters();
}
 
function toggleSuiteFilter(suiteName) {
    if (activeSuiteFilter === suiteName) {
        activeSuiteFilter = null;
        `$('.suite-row').removeClass('active');
        `$('#suiteFilterLabel').hide(); `$('#clearSuiteFilter').hide();
    } else {
        activeSuiteFilter = suiteName;
        `$('.suite-row').removeClass('active');
        `$('.suite-row[data-suite="' + suiteName + '"]').addClass('active');
        `$('#suiteFilterLabel').text(suiteName).show(); `$('#clearSuiteFilter').show();
    }
    applyFilters();
}
 
function buildSuiteTable() {
    var tbody = `$('#suiteTableBody');
    suiteTimings.forEach(function(st) {
        var badgeClass = st.Status==='Pass' ? 'badge-pass' : st.Status==='Fail' ? 'badge-fail' : 'badge-skip';
        var total = st.Pass + st.Fail + st.Skip;
        var pctPass = total > 0 ? (st.Pass/total*100) : 0;
        var pctFail = total > 0 ? (st.Fail/total*100) : 0;
        var pctSkip = total > 0 ? (st.Skip/total*100) : 0;
        var passColor = st.Pass > 0 ? '#06d6a0' : '#8888aa';
        var failColor = st.Fail > 0 ? '#ef476f' : '#8888aa';
        var skipColor = st.Skip > 0 ? '#ffd166' : '#8888aa';
        tbody.append(
            '<tr class="suite-row" data-suite="' + escapeHtml(st.Suite) + '">' +
            '<td>' + escapeHtml(st.Category) + '</td>' +
            '<td><strong>' + escapeHtml(st.Suite) + '</strong></td>' +
            '<td style="color:' + passColor + '">' + st.Pass + '</td>' +
            '<td style="color:' + failColor + '">' + st.Fail + '</td>' +
            '<td style="color:' + skipColor + '">' + st.Skip + '</td>' +
            '<td>' + st.Total + '</td>' +
            '<td><div class="progress"><div class="progress-bar progress-bar-pass" style="width:' + pctPass + '%"></div><div class="progress-bar progress-bar-fail" style="width:' + pctFail + '%"></div><div class="progress-bar progress-bar-skip" style="width:' + pctSkip + '%"></div></div></td>' +
            '<td>' + escapeHtml(st.Duration) + '</td>' +
            '<td><span class="badge ' + badgeClass + '">' + escapeHtml(st.Status) + '</span></td>' +
            '</tr>'
        );
    });
}
 
// Export helpers
function getExportRows() { return `$table.bootstrapTable('getData', { useCurrentPage:false, includeHiddenRows:true }); }
function downloadBlob(c,f,m) { saveAs(new Blob([c],{type:m}),f); }
function escapeCsvValue(v) { var s=String(v==null?'':v); return (s.indexOf('"')!==-1||s.indexOf(',')!==-1||s.indexOf('\n')!==-1)?'"'+s.replace(/"/g,'""')+'"':s; }
function exportCsv(rows) { if(!rows.length)return; var h=Object.keys(rows[0]),l=[h.join(',')]; rows.forEach(function(r){l.push(h.map(function(k){return escapeCsvValue(r[k]);}).join(','));}); downloadBlob(l.join('\r\n'),'test_results.csv','text/csv;charset=utf-8'); }
function exportJson(rows) { downloadBlob(JSON.stringify(rows,null,2),'test_results.json','application/json;charset=utf-8'); }
function exportXlsx(rows) { if(!rows.length)return; var ws=XLSX.utils.json_to_sheet(rows),wb=XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb,ws,'TestResults'); XLSX.writeFile(wb,'test_results.xlsx'); }
 
`$(function () {
    // Init bootstrap-table
    `$table.bootstrapTable({
        columns: [
            { field:'Category', title:'Category', sortable:true, searchable:true },
            { field:'Suite', title:'Suite', sortable:true, searchable:true },
            { field:'Test', title:'Test', sortable:true, searchable:true },
            { field:'Detail', title:'Detail', sortable:true, searchable:true },
            { field:'Status', title:'Status', sortable:true, searchable:true, formatter: formatTestStatus },
            { field:'Duration', title:'Duration', sortable:true, sorter: durationSorter },
            { field:'DurationMs', title:'DurationMs', sortable:false, visible:false },
            { field:'Note', title:'Note / Error', sortable:true, searchable:true, formatter: formatNote }
        ],
        data: $dataJson,
        sortName: 'Status',
        sortOrder: 'asc'
    });
    allData = `$table.bootstrapTable('getData');
    computeSummary(allData);
    buildSuiteTable();
 
    // Summary card filters
    `$('#filterTotal').on('click', function() { toggleStatusFilter('total'); });
    `$('#filterPass').on('click', function() { toggleStatusFilter('pass'); });
    `$('#filterFail').on('click', function() { toggleStatusFilter('fail'); });
    `$('#filterSkipped').on('click', function() { toggleStatusFilter('skipped'); });
 
    // Suite row click filter
    `$(document).on('click', '.suite-row', function() { toggleSuiteFilter(`$(this).data('suite')); });
    `$('#clearSuiteFilter').on('click', function() { toggleSuiteFilter(activeSuiteFilter); });
 
    // Collapse chevrons
    `$('#suiteSummary').on('show.bs.collapse', function() { `$('#suiteChevron').removeClass('bi-chevron-right').addClass('bi-chevron-down'); })
                      .on('hide.bs.collapse', function() { `$('#suiteChevron').removeClass('bi-chevron-down').addClass('bi-chevron-right'); });
    `$('#failedBody').on('show.bs.collapse', function() { `$('#failedChevron').removeClass('bi-chevron-right').addClass('bi-chevron-down'); })
                    .on('hide.bs.collapse', function() { `$('#failedChevron').removeClass('bi-chevron-down').addClass('bi-chevron-right'); });
 
    // Row counter
    var `$toolbar = `$('.fixed-table-toolbar');
    if (`$toolbar.length) {
        `$toolbar.prepend('<span class="row-counter" id="rowCounter"></span>');
        var `$btnGroup = `$toolbar.find('.columns');
        if (`$btnGroup.length) {
            var `$exportBtn = `$('<div class="btn-group"><button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown"><i class="bi bi-download"></i> Export</button>' +
                '<ul class="dropdown-menu dropdown-menu-end"><li><a class="dropdown-item" href="#" data-type="csv">CSV</a></li><li><a class="dropdown-item" href="#" data-type="json">JSON</a></li><li><a class="dropdown-item" href="#" data-type="xlsx">XLSX</a></li></ul></div>');
            `$btnGroup.append(`$exportBtn);
        }
    }
    function updateRowCounter() {
        var shown = `$table.bootstrapTable('getData').length;
        `$('#rowCounter').text(shown === allData.length ? 'Showing ' + allData.length + ' rows' : 'Showing ' + shown + ' of ' + allData.length + ' rows');
    }
    `$table.on('post-body.bs.table', updateRowCounter);
    `$(document).on('click', '.dropdown-item[data-type]', function(e) {
        e.preventDefault(); var t=`$(this).data('type'), r=getExportRows();
        if (t==='csv') exportCsv(r); else if (t==='json') exportJson(r); else if (t==='xlsx') exportXlsx(r);
    });
    updateRowCounter();
});
</script>
</body>
</html>
"@


$reportPath = Join-Path $OutputPath "MasterE2E-Report-${timestamp}.html"
[System.IO.File]::WriteAllText($reportPath, $html, [System.Text.UTF8Encoding]::new($true))

Write-Host " Dashboard: $reportPath" -ForegroundColor Green

if ($OpenReport -and (Test-Path $reportPath)) {
    Start-Process $reportPath
}

# ============================================================================
#region Return results
# ============================================================================

$masterResults

# SIG # Begin signature block
# MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCdKe6CjGEb7o8P
# 78+e5ZsT5F0hYYVXHvRLSwfMhN8ZBKCCJQ0wggVvMIIEV6ADAgECAhBI/JO0YFWU
# jTanyYqJ1pQWMA0GCSqGSIb3DQEBDAUAMHsxCzAJBgNVBAYTAkdCMRswGQYDVQQI
# DBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoM
# EUNvbW9kbyBDQSBMaW1pdGVkMSEwHwYDVQQDDBhBQUEgQ2VydGlmaWNhdGUgU2Vy
# dmljZXMwHhcNMjEwNTI1MDAwMDAwWhcNMjgxMjMxMjM1OTU5WjBWMQswCQYDVQQG
# EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0aWdv
# IFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQCN55QSIgQkdC7/FiMCkoq2rjaFrEfUI5ErPtx94jGgUW+s
# hJHjUoq14pbe0IdjJImK/+8Skzt9u7aKvb0Ffyeba2XTpQxpsbxJOZrxbW6q5KCD
# J9qaDStQ6Utbs7hkNqR+Sj2pcaths3OzPAsM79szV+W+NDfjlxtd/R8SPYIDdub7
# P2bSlDFp+m2zNKzBenjcklDyZMeqLQSrw2rq4C+np9xu1+j/2iGrQL+57g2extme
# me/G3h+pDHazJyCh1rr9gOcB0u/rgimVcI3/uxXP/tEPNqIuTzKQdEZrRzUTdwUz
# T2MuuC3hv2WnBGsY2HH6zAjybYmZELGt2z4s5KoYsMYHAXVn3m3pY2MeNn9pib6q
# RT5uWl+PoVvLnTCGMOgDs0DGDQ84zWeoU4j6uDBl+m/H5x2xg3RpPqzEaDux5mcz
# mrYI4IAFSEDu9oJkRqj1c7AGlfJsZZ+/VVscnFcax3hGfHCqlBuCF6yH6bbJDoEc
# QNYWFyn8XJwYK+pF9e+91WdPKF4F7pBMeufG9ND8+s0+MkYTIDaKBOq3qgdGnA2T
# OglmmVhcKaO5DKYwODzQRjY1fJy67sPV+Qp2+n4FG0DKkjXp1XrRtX8ArqmQqsV/
# AZwQsRb8zG4Y3G9i/qZQp7h7uJ0VP/4gDHXIIloTlRmQAOka1cKG8eOO7F/05QID
# AQABo4IBEjCCAQ4wHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYD
# VR0OBBYEFDLrkpr/NZZILyhAQnAgNpFcF4XmMA4GA1UdDwEB/wQEAwIBhjAPBgNV
# HRMBAf8EBTADAQH/MBMGA1UdJQQMMAoGCCsGAQUFBwMDMBsGA1UdIAQUMBIwBgYE
# VR0gADAIBgZngQwBBAEwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21v
# ZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEE
# KDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZI
# hvcNAQEMBQADggEBABK/oe+LdJqYRLhpRrWrJAoMpIpnuDqBv0WKfVIHqI0fTiGF
# OaNrXi0ghr8QuK55O1PNtPvYRL4G2VxjZ9RAFodEhnIq1jIV9RKDwvnhXRFAZ/ZC
# J3LFI+ICOBpMIOLbAffNRk8monxmwFE2tokCVMf8WPtsAO7+mKYulaEMUykfb9gZ
# pk+e96wJ6l2CxouvgKe9gUhShDHaMuwV5KZMPWw5c9QLhTkg4IUaaOGnSDip0TYl
# d8GNGRbFiExmfS9jzpjoad+sPKhdnckcW67Y8y90z7h+9teDnRGWYpquRRPaf9xH
# +9/DUp/mBlXpnYzyOmJRvOwkDynUWICE5EV7WtgwggWNMIIEdaADAgECAhAOmxiO
# +dAt5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYD
# VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAi
# BgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAw
# MDBaFw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdp
# Q2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERp
# Z2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
# AgoCggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsb
# hA3EMB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iT
# cMKyunWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGb
# NOsFxl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclP
# XuU15zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCr
# VYJBMtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFP
# ObURWBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTv
# kpI6nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWM
# cCxBYKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls
# 5Q5SUUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBR
# a2+xq4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6
# MIIBNjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qY
# rhwPTzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8E
# BAMCAYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k
# aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDig
# NoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v
# dENBLmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCg
# v0NcVec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQT
# SnovLbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh
# 65ZyoUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSw
# uKFWjuyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAO
# QGPFmCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjD
# TZ9ztwGpn1eqXijiuZQwggYaMIIEAqADAgECAhBiHW0MUgGeO5B5FSCJIRwKMA0G
# CSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExp
# bWl0ZWQxLTArBgNVBAMTJFNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBSb290
# IFI0NjAeFw0yMTAzMjIwMDAwMDBaFw0zNjAzMjEyMzU5NTlaMFQxCzAJBgNVBAYT
# AkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNlY3RpZ28g
# UHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwggGiMA0GCSqGSIb3DQEBAQUAA4IB
# jwAwggGKAoIBgQCbK51T+jU/jmAGQ2rAz/V/9shTUxjIztNsfvxYB5UXeWUzCxEe
# AEZGbEN4QMgCsJLZUKhWThj/yPqy0iSZhXkZ6Pg2A2NVDgFigOMYzB2OKhdqfWGV
# oYW3haT29PSTahYkwmMv0b/83nbeECbiMXhSOtbam+/36F09fy1tsB8je/RV0mIk
# 8XL/tfCK6cPuYHE215wzrK0h1SWHTxPbPuYkRdkP05ZwmRmTnAO5/arnY83jeNzh
# P06ShdnRqtZlV59+8yv+KIhE5ILMqgOZYAENHNX9SJDm+qxp4VqpB3MV/h53yl41
# aHU5pledi9lCBbH9JeIkNFICiVHNkRmq4TpxtwfvjsUedyz8rNyfQJy/aOs5b4s+
# ac7IH60B+Ja7TVM+EKv1WuTGwcLmoU3FpOFMbmPj8pz44MPZ1f9+YEQIQty/NQd/
# 2yGgW+ufflcZ/ZE9o1M7a5Jnqf2i2/uMSWymR8r2oQBMdlyh2n5HirY4jKnFH/9g
# Rvd+QOfdRrJZb1sCAwEAAaOCAWQwggFgMB8GA1UdIwQYMBaAFDLrkpr/NZZILyhA
# QnAgNpFcF4XmMB0GA1UdDgQWBBQPKssghyi47G9IritUpimqF6TNDDAOBgNVHQ8B
# Af8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNVHSUEDDAKBggrBgEFBQcD
# AzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEsGA1UdHwREMEIwQKA+oDyG
# Omh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVTaWduaW5n
# Um9vdFI0Ni5jcmwwewYIKwYBBQUHAQEEbzBtMEYGCCsGAQUFBzAChjpodHRwOi8v
# Y3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RSNDYu
# cDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG
# 9w0BAQwFAAOCAgEABv+C4XdjNm57oRUgmxP/BP6YdURhw1aVcdGRP4Wh60BAscjW
# 4HL9hcpkOTz5jUug2oeunbYAowbFC2AKK+cMcXIBD0ZdOaWTsyNyBBsMLHqafvIh
# rCymlaS98+QpoBCyKppP0OcxYEdU0hpsaqBBIZOtBajjcw5+w/KeFvPYfLF/ldYp
# mlG+vd0xqlqd099iChnyIMvY5HexjO2AmtsbpVn0OhNcWbWDRF/3sBp6fWXhz7Dc
# ML4iTAWS+MVXeNLj1lJziVKEoroGs9Mlizg0bUMbOalOhOfCipnx8CaLZeVme5yE
# Lg09Jlo8BMe80jO37PU8ejfkP9/uPak7VLwELKxAMcJszkyeiaerlphwoKx1uHRz
# NyE6bxuSKcutisqmKL5OTunAvtONEoteSiabkPVSZ2z76mKnzAfZxCl/3dq3dUNw
# 4rg3sTCggkHSRqTqlLMS7gjrhTqBmzu1L90Y1KWN/Y5JKdGvspbOrTfOXyXvmPL6
# E52z1NZJ6ctuMFBQZH3pwWvqURR8AgQdULUvrxjUYbHHj95Ejza63zdrEcxWLDX6
# xWls/GDnVNueKjWUH3fTv1Y8Wdho698YADR7TNx8X8z2Bev6SivBBOHY+uqiirZt
# g0y9ShQoPzmCcn63Syatatvx157YK9hlcPmVoa1oDE5/L9Uo2bC5a4CH2RwwggY+
# MIIEpqADAgECAhAHnODk0RR/hc05c892LTfrMA0GCSqGSIb3DQEBDAUAMFQxCzAJ
# BgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNl
# Y3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwHhcNMjYwMjA5MDAwMDAw
# WhcNMjkwNDIxMjM1OTU5WjBVMQswCQYDVQQGEwJVUzEUMBIGA1UECAwLQ29ubmVj
# dGljdXQxFzAVBgNVBAoMDkphc29uIEFsYmVyaW5vMRcwFQYDVQQDDA5KYXNvbiBB
# bGJlcmlubzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPN6aN4B1yYW
# kI5b5TBj3I0VV/peETrHb6EY4BHGxt8Ap+eT+WpEpJyEtRYPxEmNJL3A38Bkg7mw
# zPE3/1NK570ZBCuBjSAn4mSDIgIuXZnvyBO9W1OQs5d67MlJLUAEufl18tOr3ST1
# DeO9gSjQSAE5Nql0QDxPnm93OZBon+Fz3CmE+z3MwAe2h4KdtRAnCqwM+/V7iBdb
# w+JOxolpx+7RVjGyProTENIG3pe/hKvPb501lf8uBAADLdjZr5ip8vIWbf857Yw1
# Bu10nVI7HW3eE8Cl5//d1ribHlzTzQLfttW+k+DaFsKZBBL56l4YAlIVRsrOiE1k
# dHYYx6IGrEA809R7+TZA9DzGqyFiv9qmJAbL4fDwetDeyIq+Oztz1LvEdy8Rcd0J
# BY+J4S0eDEFIA3X0N8VcLeAwabKb9AjulKXwUeqCJLvN79CJ90UTZb2+I+tamj0d
# n+IKMEsJ4v4Ggx72sxFr9+6XziodtTg5Luf2xd6+PhhamOxF2px9LObhBLLEMyRs
# CHZIzVZOFKu9BpHQH7ufGB+Sa80Tli0/6LEyn9+bMYWi2ttn6lLOPThXMiQaooRU
# q6q2u3+F4SaPlxVFLI7OJVMhar6nW6joBvELTJPmANSMjDSRFDfHRCdGbZsL/keE
# LJNy+jZctF6VvxQEjFM8/bazu6qYhrA7AgMBAAGjggGJMIIBhTAfBgNVHSMEGDAW
# gBQPKssghyi47G9IritUpimqF6TNDDAdBgNVHQ4EFgQU6YF0o0D5AVhKHbVocr8G
# aSIBibAwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYI
# KwYBBQUHAwMwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggrBgEFBQcC
# ARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAw
# PqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVT
# aWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcwAoY4aHR0
# cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIz
# Ni5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMA0GCSqG
# SIb3DQEBDAUAA4IBgQAEIsm4xnOd/tZMVrKwi3doAXvCwOA/RYQnFJD7R/bSQRu3
# wXEK4o9SIefye18B/q4fhBkhNAJuEvTQAGfqbbpxow03J5PrDTp1WPCWbXKX8Oz9
# vGWJFyJxRGftkdzZ57JE00synEMS8XCwLO9P32MyR9Z9URrpiLPJ9rQjfHMb1BUd
# vaNayomm7aWLAnD+X7jm6o8sNT5An1cwEAob7obWDM6sX93wphwJNBJAstH9Ozs6
# LwISOX6sKS7CKm9N3Kp8hOUue0ZHAtZdFl6o5u12wy+zzieGEI50fKnN77FfNKFO
# WKlS6OJwlArcbFegB5K89LcE5iNSmaM3VMB2ADV1FEcjGSHw4lTg1Wx+WMAMdl/7
# nbvfFxJ9uu5tNiT54B0s+lZO/HztwXYQUczdsFon3pjsNrsk9ZlalBi5SHkIu+F6
# g7tWiEv3rtVApmJRnLkUr2Xq2a4nbslUCt4jKs5UX4V1nSX8OM++AXoyVGO+iTj7
# z+pl6XE9Gw/Td6WKKKswgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0G
# CSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTla
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEy
# NTYgMjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHT
# CphBcr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPh
# of6pvF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mA
# xAHeHYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBv
# MgEdgkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps
# 0wjUjsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF
# 83bRVFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXi
# UOeSLsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOM
# CZIVNSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydP
# pOjL6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrU
# G2ZdSoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+
# sdFUeEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0T
# AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYD
# VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG
# A1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV
# HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU
# cnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s
# BwEwDQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WI
# GjB/T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+
# IQhQE7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8M
# yb9rEVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2
# th9y1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjaj
# V/gxdEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2
# Lr3ty9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFze
# GxcytL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG
# 7uEBYTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+N
# Jpud/v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckT
# etiSuEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszW
# kPZPubdcMIIG7TCCBNWgAwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0B
# AQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/
# BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg
# U0hBMjU2IDIwMjUgQ0ExMB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVow
# YzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQD
# EzJEaWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIw
# MjUgMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5g
# VrMrV7pvUf+GcAoB38o3zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN
# +vo8dE2/pPvOx/Vj8TchTySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qo
# me7MrxVyfQO9sMx6ZAWjFDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ/
# /nBZZREr4h/GI6Dxb2UoyrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouT
# MYFOnHoRh6+86Ltc5zjPKHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8
# DD+nigNJFmt6LAHvH3KSuNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnp
# JeItK/DhKbPxTTuGoX7wJNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP5
# 1ho1zBp+xUIZkpSFA8vWdoUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49
# kPmk8iyyizNDIXj//cOgrY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5P
# WPsWeupWs7NpChUk555K096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7Y
# ufAkprxMiXAJQ1XCmnCfgPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAA
# MB0GA1UdDgQWBBTkO/zyMe39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK
# 6eQGfHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYI
# KwYBBQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8v
# b2NzcC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZT
# SEEyNTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5k
# aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2
# U0hBMjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9
# bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP
# 2zWLpQq1b4URGnwWBdEZD9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O
# 6Lgjg8K8elC4+oWCqnU/ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskg
# iC3QYIUP2S3HQvHG1FDu+WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMU
# BaB5bdrPbF6MRYs03h4obEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDF
# kxUGtMTaiLR9wjxUxu2hECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+
# zJNEsuEB7O7/cuvTQasnM9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/l
# wd6UArb+BOVAkg2oOvol/DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxl
# RcGG0LIhp6GvReQGgMgYxQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2
# zRWVFjF7mcr4C34Mj3ocCVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJg
# baP5t2nGj/ULLi49xTcBZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOC
# IUjsarfNZzGCBkQwggZAAgEBMGgwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1Nl
# Y3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWdu
# aW5nIENBIFIzNgIQB5zg5NEUf4XNOXPPdi036zANBglghkgBZQMEAgEFAKCBhDAY
# BgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3
# AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEi
# BCAOF3X7xfIEx1S0BsmLEUTycoVj+h1VKf3TA0au1xBpjzANBgkqhkiG9w0BAQEF
# AASCAgBx33tj6JPosfpg21hiTAWCudAAkD4Rc+InzRddeoj99DlalGsJ5fofpIP5
# OzGkslj+eoaC2tpMmj0ONYJgjF+2pg3TFSqr6rHZ/RFcqHaA9Bblp6bQGiROPXBi
# Sl4J5VqcT8FCaxbMMukGWvW2F1RAFtZQjV459bGxHdzoWpVh7wqKTCtoex9VKuW0
# H+XXR0OmqRqJJxwYTYMbk5Q65QTNmh4I3ziWwF5EXwZIpEtleJdLRA93bKL8sdco
# CSNAejCTetnSmdeNjHdFilQblKzfo3NqMotGegDPFA/C/vJYiTxk1Dd3QXl/9r/J
# BmKLsME72fXIzI6no8RjelgbX34DLC4nBsgIWYrHGv/VY2gM4lSeRmh1pO2VQ4Lb
# 8HAs/pS5618V2Y/lYmzMARPLmX54T1tYPyl3NQJKFzYN1fLB5P1WInMTePBLY7qt
# NM1SQZryqPK4D7lMlxMK5mAxemJw/lXEIoHLpNHkXJRzmQtbxBrWVeqirkUQcnAA
# yz920FVWzsbZPH0Ij3vtT0k6PabqK1CxsESIK5iOvhQ27NRSwD+qZZ+qgvZtp5Zk
# ty0WdE8igoN0lXLfzJDnpQ8QNPjyXoh7ugc5mrPmwqZSpwhovNPqug9LAYroXC0C
# AiVC6/4w8o4iEe/WzXwpoK64nY5mrWOmJVNYVFQ4Gg4gX/j/6KGCAyYwggMiBgkq
# hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E
# aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l
# U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeV
# dGgwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG
# CSqGSIb3DQEJBTEPFw0yNjA1MjQxNzQxMTdaMC8GCSqGSIb3DQEJBDEiBCCWxQHm
# hhAjv+zDzIIQ8LzlrN8fipWdSLQ3Aj5k0yKiajANBgkqhkiG9w0BAQEFAASCAgAc
# DZ3NkKrELcZ1LQeOUbzKbHSwyc9LZbGDjQAC/t0E2/pyO32oGnfitnp/7fo1wxST
# lNrRW49/XN0tYDpwS22M9QV+5keDaKzVcTL/KKosd65Im0sETMB/pVprh4uJ9fqg
# 11gV6DDEDdahmGn0PQbGHBQDiywGnwoGI1iEj0/eKY1NVsxKKS1+YCwupEgejVom
# 1J6Idtdk/yIFhjoGI86w02yXd3fhELa9Uy+t9lmjVQDRt9ZS6FZcxw+ha9lo2HcP
# Tthw5EkJNJg4RSuuqoRn52+zPAufTy7jBGGJFC1alxytA9fA/5kwKUiC0vl3Mkdy
# RcwjbpYr/XJCpwMQYc+7KdErDVSO/D5qm8K93sOasLm4PUAoiGcC1OuWvHABJv+e
# +U5l9uOg/32mY4u8vA/dQX5Ex2GAvNNLpGIc6S6K4JU2fF9Mu334N1XlH6d83X6N
# 23dKoXlSqvJ/pbrdu3eyMEqJdxmpmSOa7+D3jzuF4hltKRPoRAFWIiCGmuGww/e8
# W1D+v8FJYDStp1iulSjcq4fOpyCqHOtSRZeMjqwfs/Xad4PzZ4yAYcdvKeAInvcV
# VqsJxYjSY9fbUBwdAqTtPI7yLEelQCHfZmY/gTM5x0O4rUpdyrqzzNBIAQvhjBYL
# //Pwd8BCXAS6UaKf+y7a5a7irLIyg6JtSynK+gNttg==
# SIG # End signature block