helpers/discovery/Setup-OCI-Discovery.ps1

<#
.SYNOPSIS
    OCI Discovery -- Discover Oracle Cloud compute, databases, and load balancers,
    then optionally push to WhatsUp Gold.
 
.DESCRIPTION
    Interactive script that discovers Oracle Cloud Infrastructure resources via
    the OCI.PSModules, then lets you choose what to do with the results:
 
      [1] Push devices to WhatsUp Gold (Ping monitors for public IPs)
      [2] Export discovery plan to JSON
      [3] Export discovery plan to CSV
      [4] Show full plan table in console
      [5] Generate HTML dashboard with metric snapshots
      [6] Exit
      [7] Dashboard + Push to WUG
 
    Monitor Strategy (Option B -- built-in WUG monitors):
      OCI REST APIs require HTTP Signature authentication which WUG cannot
      perform natively. Instead, this script uses built-in WUG Ping monitors
      for resources with public IPs. Resources without public IPs are included
      as inventory items in the dashboard only.
 
    Architecture (when pushed to WUG):
      [OCI Compute device in WUG]
          '-- Ping monitor (default, auto-added by WUG)
      [OCI Load Balancer device in WUG]
          '-- Ping monitor (default, auto-added by WUG)
      Resources without public IPs:
          '-- Dashboard inventory only (no WUG device created)
 
    OCI Monitoring metrics (CPU, memory, disk, network) are queried via
    OCI.PSModules during discovery and stored as device attributes for
    dashboard display. The OCI CLI is optional for metrics enrichment.
 
    First Run:
      1. Prompts for OCI config file path and profile name
      2. Prompts for tenancy OCID
      3. Stores config path in DPAPI vault (encrypted)
      4. Validates OCI connectivity and discovers resources
      5. Shows summary, then asks what to do
 
    Subsequent Runs:
      Loads OCI config path from vault and reconnects.
 
    Prerequisites:
      1. OCI.PSModules installed (Install-Module OCI.PSModules)
      2. OCI config file (~/.oci/config) with valid API key
      3. Appropriate IAM policies for read access
 
.PARAMETER TenancyId
    OCI tenancy OCID. Required for non-interactive mode.
 
.PARAMETER ConfigFile
    Path to OCI config file. Default: ~/.oci/config.
 
.PARAMETER Profile
    OCI config profile name. Default: DEFAULT.
 
.PARAMETER Region
    Override OCI region for discovery.
 
.PARAMETER Action
    What to do with discovery results. When specified, skips the interactive menu.
    Valid values: PushToWUG, ExportJSON, ExportCSV, ShowTable, None.
 
.PARAMETER WUGServer
    WhatsUp Gold server address. Default: 192.168.74.74.
 
.PARAMETER WUGCredential
    PSCredential for WhatsUp Gold admin login (non-interactive WUG push).
 
.PARAMETER NonInteractive
    Suppress all prompts. Uses cached vault credentials and parameter defaults.
 
.NOTES
    OCI.PSModules handles API request signing via config file for discovery.
    WUG monitors use built-in Ping (no cloud auth needed).
    OCI metrics are captured as point-in-time snapshots during discovery
    and displayed in the dashboard.
 
.EXAMPLE
    .\Setup-OCI-Discovery.ps1
    # Interactive mode.
 
.EXAMPLE
    .\Setup-OCI-Discovery.ps1 -TenancyId 'ocid1.tenancy.oc1..aaaaaa' -Action PushToWUG -NonInteractive
    # Scheduled mode.
 
.EXAMPLE
    .\Setup-OCI-Discovery.ps1 -TenancyId 'ocid1.tenancy.oc1..aaa' -Region 'us-ashburn-1' -Action ExportJSON
    # Discover in specific region and export.
#>

[CmdletBinding()]
param(
    [Alias('Target')]
    [string]$TenancyId,

    [string]$ConfigFile,

    [string]$Profile = 'DEFAULT',

    [string]$Region,

    [ValidateSet('PushToWUG', 'ExportJSON', 'ExportCSV', 'ShowTable', 'Dashboard', 'DashboardAndPush', 'None')]
    [string]$Action,

    [string]$WUGServer = '192.168.74.74',

    [PSCredential]$WUGCredential,

    [string]$OutputPath,

    # --- Opt-in cost, quota and waste analysis -------------------------------
    [switch]$IncludeCostAndQuota,

    [hashtable]$UnitPrice,

    [string]$CostReportPath,

    [switch]$NonInteractive
)

# --- Output directory ---------------------------------------------------------
if (-not $OutputPath) {
    if ($NonInteractive) {
        $OutputPath = Join-Path $env:LOCALAPPDATA 'WhatsUpGoldPS\DiscoveryHelpers\Output'
    } else {
        $OutputPath = $env:TEMP
    }
}
if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null }
$OutputDir = $OutputPath

# --- Load helpers -------------------------------------------------------------
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
. (Join-Path $scriptDir 'DiscoveryHelpers.ps1')
. (Join-Path $scriptDir 'DiscoveryProvider-OCI.ps1')

# Load dynamic dashboard generator
$dynDashPath = Join-Path (Split-Path $scriptDir -Parent) 'reports\Export-DynamicDashboardHtml.ps1'
if (Test-Path $dynDashPath) { . $dynDashPath }

# ==============================================================================
# STEP 1: Gather target info
# ==============================================================================
Write-Host "=== OCI Discovery ===" -ForegroundColor Cyan
Write-Host ""

# Resolve config file
if (-not $ConfigFile) {
    $defaultConfig = Join-Path $env:USERPROFILE '.oci\config'
    if ($NonInteractive) {
        $ConfigFile = $defaultConfig
    }
    else {
        $cfgInput = Read-Host -Prompt "OCI config file [default: $defaultConfig]"
        $ConfigFile = if ([string]::IsNullOrWhiteSpace($cfgInput)) { $defaultConfig } else { $cfgInput.Trim() }
    }
}

if (-not (Test-Path $ConfigFile)) {
    Write-Error "OCI config file not found: $ConfigFile"
    return
}

# Resolve profile
if (-not $PSBoundParameters.ContainsKey('Profile') -and -not $NonInteractive) {
    $profInput = Read-Host -Prompt "OCI profile [default: $Profile]"
    if (-not [string]::IsNullOrWhiteSpace($profInput)) { $Profile = $profInput.Trim() }
}

# Resolve tenancy ID
if (-not $TenancyId) {
    if ($NonInteractive) {
        # Try to extract from config file
        try {
            $configContent = Get-Content $ConfigFile -Raw
            if ($configContent -match 'tenancy\s*=\s*(ocid1\.tenancy\.[^\s]+)') {
                $TenancyId = $Matches[1]
            }
        }
        catch { }
        if (-not $TenancyId) { Write-Error 'No tenancy ID specified. Pass -TenancyId parameter.'; return }
    }
    else {
        # Try to extract default from config
        $defaultTenancy = $null
        try {
            $configContent = Get-Content $ConfigFile -Raw
            if ($configContent -match 'tenancy\s*=\s*(ocid1\.tenancy\.[^\s]+)') {
                $defaultTenancy = $Matches[1]
            }
        }
        catch { }
        $tenancyPrompt = if ($defaultTenancy) { "Tenancy OCID [default: $defaultTenancy]" } else { "Tenancy OCID" }
        $tenancyInput = Read-Host -Prompt $tenancyPrompt
        $TenancyId = if ([string]::IsNullOrWhiteSpace($tenancyInput) -and $defaultTenancy) { $defaultTenancy } else { $tenancyInput.Trim() }
    }
}

if (-not $TenancyId) { Write-Error 'No tenancy OCID provided.'; return }
Write-Host "Tenancy: $TenancyId" -ForegroundColor Cyan
Write-Host "Config: $ConfigFile (profile: $Profile)" -ForegroundColor Cyan
if ($Region) { Write-Host "Region: $Region" -ForegroundColor Cyan }
Write-Host ""

# ==============================================================================
# STEP 2: Authentication (OCI config file)
# ==============================================================================
Write-Host "Validating OCI connectivity..." -ForegroundColor Cyan
try {
    $connectSplat = @{}
    if ($ConfigFile) { $connectSplat['ConfigFile'] = $ConfigFile }
    if ($Profile -and $Profile -ne 'DEFAULT') { $connectSplat['Profile'] = $Profile }
    Connect-OCIProfile @connectSplat
    Write-Host "OCI authentication successful." -ForegroundColor Green
}
catch {
    Write-Error "OCI authentication failed: $_"
    return
}
Write-Host ""

# ==============================================================================
# STEP 3: Discover -- query OCI APIs
# ==============================================================================
Write-Host "Querying OCI tenancy..." -ForegroundColor Cyan

$ociCredential = @{
    ConfigFile = $ConfigFile
    Profile    = $Profile
    TenancyId  = $TenancyId
}
if ($Region) { $ociCredential['Region'] = $Region }

$plan = Invoke-Discovery -ProviderName 'OCI' `
    -Target $TenancyId `
    -Credential $ociCredential

if (-not $plan -or $plan.Count -eq 0) {
    Write-Warning "No cloud resources discovered in tenancy."
    $plan = @()
}

# ==============================================================================
# STEP 3b: Cost, quota and waste analysis (opt-in)
# ==============================================================================
$costFindings = @()
if ($IncludeCostAndQuota) {
    $costHelpers = Join-Path $scriptDir 'CloudCostHelpers.ps1'
    if (-not (Test-Path $costHelpers)) {
        Write-Warning "CloudCostHelpers.ps1 not found; skipping cost and quota analysis."
    }
    else {
        . $costHelpers
        Write-Host ""
        Write-Host "Analyzing OCI cost, quota and waste..." -ForegroundColor Cyan

        $compartments = @($plan | ForEach-Object { $_.Attributes['OCI.CompartmentId'] } | Where-Object { $_ } | Sort-Object -Unique)
        if ($compartments.Count -eq 0 -and $TenancyId) { $compartments = @($TenancyId) }

        foreach ($compartmentId in $compartments) {
            Write-Host " Compartment $compartmentId..." -ForegroundColor DarkGray
            try {
                $costSplat = @{ CompartmentId = $compartmentId; CompartmentName = $compartmentId; Region = $Region }
                if ($UnitPrice) { $costSplat['UnitPrice'] = $UnitPrice }
                $costFindings += @(Get-OCICostQuotaFinding @costSplat)
            }
            catch {
                Write-Warning " Cost analysis failed for ${compartmentId}: $($_.Exception.Message)"
            }
        }

        if ($costFindings.Count -gt 0) {
            Get-CloudCostSummary -Finding $costFindings | Format-Table -AutoSize | Out-String -Width 160 | Write-Host
            $costFindings | Where-Object { $_.Status -eq 'Fail' -or $_.Status -eq 'Warn' } |
                Select-Object Category, Check, Status, Value, Resource |
                Format-Table -AutoSize | Out-String -Width 160 | Write-Host

            if (-not $CostReportPath) { $CostReportPath = Join-Path $OutputDir 'OCI-Cost-Quota.html' }
            try {
                Export-CloudCostDashboard -Finding $costFindings -OutputPath $CostReportPath -ReportTitle 'OCI Cost and Quota' | Out-Null
                Write-Host " Cost report: $CostReportPath" -ForegroundColor Green
            }
            catch {
                Write-Warning " Could not render cost dashboard: $($_.Exception.Message)"
            }
        }
        else {
            Write-Host " No cost or quota findings returned." -ForegroundColor DarkGray
        }
    }
}

# ==============================================================================
# STEP 4: Show the plan
# ==============================================================================
$devicePlan = [ordered]@{}

foreach ($item in $plan) {
    $devType = $item.Attributes['OCI.DeviceType']
    $name = $item.Attributes['ComputedDisplayName']
    $ip   = $item.Attributes['OCI.IPAddress']
    switch ($devType) {
        'Compute'      { $key = "compute:$($item.Attributes['OCI.InstanceId'])" }
        'DBSystem'     { $key = "db:$($item.Attributes['OCI.DBSystemId'])" }
        'AutonomousDB' { $key = "adb:$($item.Attributes['OCI.AutonomousDbId'])" }
        'LoadBalancer'  { $key = "lb:$($item.Attributes['OCI.LoadBalancerId'])" }
        default { continue }
    }
    if (-not $devicePlan.Contains($key)) {
        $devicePlan[$key] = @{
            Name  = $name
            IP    = $ip
            Type  = $devType
            Attrs = $item.Attributes
            Items = [System.Collections.ArrayList]@()
        }
    }
    [void]$devicePlan[$key].Items.Add($item)
}

$computeDevices = @($devicePlan.Values | Where-Object { $_.Type -eq 'Compute' })
$dbDevices      = @($devicePlan.Values | Where-Object { $_.Type -eq 'DBSystem' })
$adbDevices     = @($devicePlan.Values | Where-Object { $_.Type -eq 'AutonomousDB' })
$lbDevices      = @($devicePlan.Values | Where-Object { $_.Type -eq 'LoadBalancer' })

# Separate monitorable items from inventory-only
$monitorableItems = @($plan | Where-Object { -not $_.MonitorParams._InventoryOnly })
$inventoryItems   = @($plan | Where-Object { $_.MonitorParams._InventoryOnly })
$activeTemplates  = @($monitorableItems | Where-Object { $_.ItemType -eq 'ActiveMonitor' } | Select-Object -ExpandProperty Name -Unique)

Write-Host ""
Write-Host "Discovery complete!" -ForegroundColor Green
Write-Host " Compute Instances: $($computeDevices.Count)" -ForegroundColor White
Write-Host " DB Systems: $($dbDevices.Count)" -ForegroundColor White
Write-Host " Autonomous Databases: $($adbDevices.Count)" -ForegroundColor White
Write-Host " Load Balancers: $($lbDevices.Count)" -ForegroundColor White
Write-Host " Monitorable items: $($monitorableItems.Count) (Ping/TCP)" -ForegroundColor White
Write-Host " Inventory-only items: $($inventoryItems.Count) (no public IP)" -ForegroundColor White
Write-Host " Total plan items: $($plan.Count)" -ForegroundColor White
Write-Host ""

if ($activeTemplates.Count -gt 0) {
    Write-Host "Monitor templates:" -ForegroundColor Cyan
    foreach ($t in $activeTemplates) { Write-Host " [Active] $t" -ForegroundColor White }
    Write-Host ""
}

$devicePlan.Values | Sort-Object @{E={$_.Type}}, @{E={$_.Name}} |
    ForEach-Object {
        $monCount = @($_.Items | Where-Object { -not $_.MonitorParams._InventoryOnly }).Count
        $invCount = @($_.Items | Where-Object { $_.MonitorParams._InventoryOnly }).Count
        [PSCustomObject]@{
            Device    = $_.Name
            Type      = $_.Type
            IP        = if ($_.IP) { $_.IP } else { '(no public IP)' }
            Monitors  = $monCount
            Inventory = if ($invCount -gt 0) { 'yes' } else { '' }
        }
    } |
    Format-Table -AutoSize

# ==============================================================================
# STEP 5: Export or push to WUG
# ==============================================================================
$choice = $null
if ($Action) {
    switch ($Action) {
        'PushToWUG'        { $choice = '1' }
        'ExportJSON'       { $choice = '2' }
        'ExportCSV'        { $choice = '3' }
        'ShowTable'        { $choice = '4' }
        'Dashboard'        { $choice = '5' }
        'None'             { $choice = '6' }
        'DashboardAndPush' { $choice = '7' }
    }
}

if (-not $choice -and $NonInteractive) { $choice = '5' }

if (-not $choice) {
    Write-Host "What would you like to do?" -ForegroundColor Cyan
    Write-Host " [1] Push monitors to WhatsUp Gold"
    Write-Host " [2] Export plan to JSON file"
    Write-Host " [3] Export plan to CSV file"
    Write-Host " [4] Show full plan table"
    Write-Host " [5] Generate HTML dashboard"
    Write-Host " [6] Exit (do nothing)"
    Write-Host " [7] Dashboard + Push to WUG"
    Write-Host ""
    $choice = Read-Host -Prompt "Choice [1-7]"
}

# Handle DashboardAndPush: run Dashboard then PushToWUG sequentially
if ($choice -eq '7') {
    $actionsToRun = @('5', '1')
} else {
    $actionsToRun = @($choice)
}

foreach ($currentChoice in $actionsToRun) {
switch ($currentChoice) {
    '1' {
        # --- Push to WUG (Ping/TCP monitors -- no REST API credential needed) ---
        Write-Host "Loading WhatsUpGoldPS module..." -ForegroundColor Cyan
        try {
            $repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
            $repoPsd1 = Join-Path $repoRoot 'WhatsUpGoldPS.psd1'
            if (Test-Path $repoPsd1) { Import-Module $repoPsd1 -Force -ErrorAction Stop }
            else { Import-Module WhatsUpGoldPS -ErrorAction Stop }
        }
        catch { Write-Error "Could not load WhatsUpGoldPS module: $_"; return }
        # Dot-source internal helper so scripts can call Get-WUGAPIResponse directly
        $apiResponsePath = Join-Path $PSScriptRoot '..\..\functions\Get-WUGAPIResponse.ps1'
        if (Test-Path $apiResponsePath) { . $apiResponsePath }

        if ($WUGCredential) {
            Connect-WUGServer -serverUri $WUGServer -Credential $WUGCredential -IgnoreSSLErrors
        }
        else {
            Connect-WUGServer -AutoConnect -IgnoreSSLErrors
        }

        $stats = @{ DevicesCreated = 0; DevicesFound = 0; DevicesSkipped = 0; MonitorsAssigned = 0 }
        $wugDeviceMap = @{}
        $deviceKeys = @($devicePlan.Keys | Sort-Object)

        # Filter to devices that have at least one monitorable item (has public IP)
        $monitorableDeviceKeys = @($deviceKeys | Where-Object {
            $dev = $devicePlan[$_]
            @($dev.Items | Where-Object { -not $_.MonitorParams._InventoryOnly }).Count -gt 0
        })
        $inventoryOnlyDeviceKeys = @($deviceKeys | Where-Object { $_ -notin $monitorableDeviceKeys })

        Write-Host ""
        Write-Host " Devices with public IPs (will create in WUG): $($monitorableDeviceKeys.Count)" -ForegroundColor Cyan
        Write-Host " Devices without public IPs (inventory only): $($inventoryOnlyDeviceKeys.Count)" -ForegroundColor DarkGray

        # ---- 1. Check existing vs new devices ---
        Write-Host " Checking for existing devices..." -ForegroundColor Cyan
        $existingDevices = @{}
        $newDeviceKeys = [System.Collections.Generic.List[string]]::new()

        foreach ($key in $monitorableDeviceKeys) {
            $dev = $devicePlan[$key]
            $displayName = "$($dev.Name) (OCI)"
            $deviceId = $null
            try {
                $searchResults = @(Get-WUGDevice -SearchValue $dev.Name)
                if ($searchResults.Count -gt 0) {
                    $match = $searchResults | Where-Object { $_.displayName -eq $dev.Name -or $_.displayName -eq $displayName } | Select-Object -First 1
                    if ($match) { $deviceId = $match.id }
                }
            }
            catch { }

            if ($deviceId) { $existingDevices[$key] = $deviceId; $wugDeviceMap[$key] = $deviceId; $stats.DevicesFound++ }
            else { $newDeviceKeys.Add($key) }
        }
        Write-Host " Found $($stats.DevicesFound) existing, $($newDeviceKeys.Count) new" -ForegroundColor DarkGray

        # ---- 2. Create new devices with Ping active monitor (built-in) ---
        if ($newDeviceKeys.Count -gt 0) {
            Write-Host " Creating $($newDeviceKeys.Count) devices..." -ForegroundColor Yellow
            $devIdx = 0
            foreach ($key in $newDeviceKeys) {
                $devIdx++
                $dev = $devicePlan[$key]
                $addIP = if ($dev.IP) { $dev.IP } else { '0.0.0.0' }
                $displayName = "$($dev.Name) (OCI)"

                Write-Progress -Activity 'Creating OCI devices' `
                    -Status "$devIdx / $($newDeviceKeys.Count) - $displayName" `
                    -PercentComplete ([Math]::Round(($devIdx / $newDeviceKeys.Count) * 100))

                $devAttrs = @()
                foreach ($attrName in $dev.Attrs.Keys) {
                    if ($dev.Attrs[$attrName]) { $devAttrs += @{ name = $attrName; value = "$($dev.Attrs[$attrName])" } }
                }

                # Ping is the default active monitor in WUG -- just create the device
                # with its IP address and WUG adds Ping automatically
                $splat = @{
                    displayName  = $displayName
                    DeviceAddress = $addIP
                    Hostname     = $dev.Name
                    Brand        = 'Oracle Cloud'
                    Note         = "OCI $($dev.Type) - discovered $(Get-Date -Format 'yyyy-MM-dd')"
                }
                if ($devAttrs.Count -gt 0) { $splat['Attributes'] = $devAttrs }

                try {
                    $devResult = Add-WUGDeviceTemplate @splat

                    $isErrorResult = $false
                    if ($devResult -is [array]) { $isErrorResult = $true }
                    elseif ($devResult -and $devResult.PSObject.Properties['messages']) { $isErrorResult = $true }

                    if ($devResult -and -not $isErrorResult) {
                        $newDeviceId = $null
                        if ($devResult.idMap) { $newDeviceId = ($devResult.idMap | Select-Object -First 1).resultId }
                        elseif ($devResult.PSObject.Properties['resultId']) { $newDeviceId = $devResult.resultId }
                        if ($newDeviceId) {
                            $wugDeviceMap[$key] = $newDeviceId
                            $stats.DevicesCreated++
                            Write-Verbose "Created device '$displayName' (ID: $newDeviceId)"
                        } else {
                            Write-Warning "Device '$displayName' - API returned success but no device ID."
                        }
                    } else {
                        $errMsgs = @()
                        if ($devResult -is [array]) { foreach ($e in $devResult) { if ($e.PSObject.Properties['messages']) { $errMsgs += ($e.messages -join '; ') } } }
                        elseif ($devResult -and $devResult.PSObject.Properties['messages']) { $errMsgs += ($devResult.messages -join '; ') }
                        $errText = if ($errMsgs.Count -gt 0) { $errMsgs -join ' | ' } else { 'Unknown error (no details returned)' }
                        Write-Warning "Failed to create device '$displayName': $errText"
                    }
                }
                catch { Write-Warning "Error creating device '$displayName': $_" }
            }
            Write-Progress -Activity 'Creating OCI devices' -Completed
            Write-Host " Devices created: $($stats.DevicesCreated)" -ForegroundColor Green
        }

        # ---- 3. Update attributes on existing devices ---
        if ($existingDevices.Count -gt 0) {
            Write-Host " Updating $($existingDevices.Count) existing device attributes..." -ForegroundColor Cyan
            foreach ($key in $existingDevices.Keys) {
                $dev = $devicePlan[$key]
                $devAttrs = @()
                foreach ($attrName in $dev.Attrs.Keys) {
                    if ($dev.Attrs[$attrName]) { $devAttrs += @{ name = $attrName; value = "$($dev.Attrs[$attrName])" } }
                }
                if ($devAttrs.Count -gt 0) {
                    try { Set-WUGDeviceAttribute -DeviceId ([int]$existingDevices[$key]) -Attributes $devAttrs -ErrorAction Stop }
                    catch { Write-Verbose "Could not update attributes for $($dev.Name): $_" }
                }
            }
        }

        # ---- Summary ---
        Write-Host ""
        Write-Host "Push complete!" -ForegroundColor Green
        Write-Host " Devices: $($stats.DevicesCreated) created, $($stats.DevicesFound) existing" -ForegroundColor White
        Write-Host " Inventory-only: $($inventoryOnlyDeviceKeys.Count) (no public IP, skipped)" -ForegroundColor White
        Write-Host ""
        Write-Host "WUG monitors: Ping (default) monitors network reachability." -ForegroundColor Cyan
        Write-Host "OCI metric snapshots are stored as device attributes for dashboard." -ForegroundColor Cyan
    }
    '2' {
        $jsonPath = Join-Path $OutputDir 'oci-discovery-plan.json'
        $plan | Export-DiscoveryPlan -Format JSON -Path $jsonPath -IncludeParams
        Write-Host "Exported to: $jsonPath" -ForegroundColor Green
    }
    '3' {
        $csvPath = Join-Path $OutputDir 'oci-discovery-plan.csv'
        $plan | Export-DiscoveryPlan -Format CSV -Path $csvPath
        Write-Host "Exported to: $csvPath" -ForegroundColor Green
    }
    '4' {
        $plan | Export-DiscoveryPlan -Format Table
    }
    '5' {
        # ----------------------------------------------------------------
        # Generate OCI HTML Dashboard from discovery data
        # ----------------------------------------------------------------
        Write-Host ""
        Write-Host "Building OCI dashboard from discovery data..." -ForegroundColor Cyan

        $dashboardRows = @()
        foreach ($key in $devicePlan.Keys) {
            $dev = $devicePlan[$key]
            $attrs = $dev.Attrs

            $row = [ordered]@{
                Device     = $dev.Name
                Type       = $dev.Type
                State      = if ($attrs['OCI.LifecycleState']) { $attrs['OCI.LifecycleState'] } else { 'Unknown' }
                IP         = if ($dev.IP) { $dev.IP } else { '(no public IP)' }
                Shape      = if ($attrs['OCI.Shape']) { $attrs['OCI.Shape'] } else { 'N/A' }
                Region     = if ($attrs['OCI.Region']) { $attrs['OCI.Region'] } else { 'default' }
                AD         = if ($attrs['OCI.AD']) { $attrs['OCI.AD'] } else { 'N/A' }
                Created    = if ($attrs['OCI.TimeCreated']) { $attrs['OCI.TimeCreated'] } else { 'N/A' }
                Monitors   = @($dev.Items | Where-Object { -not $_.MonitorParams._InventoryOnly }).Count
            }

            # Add metric snapshots if available
            foreach ($attrKey in @($attrs.Keys | Sort-Object)) {
                if ($attrKey -match '^OCI\.Metric\.(.+)$') {
                    $metricName = $Matches[1]
                    $row[$metricName] = $attrs[$attrKey]
                }
            }

            $dashboardRows += [PSCustomObject]$row
        }

        if ($dashboardRows.Count -eq 0) {
            # Generate a summary dashboard even with 0 resources
            $configRegion = 'default'
            try {
                $cfgContent = Get-Content $ConfigFile -Raw
                if ($cfgContent -match 'region\s*=\s*(\S+)') { $configRegion = $Matches[1] }
            } catch { }
            $dashboardRows = @(
                [PSCustomObject][ordered]@{
                    Device   = "(tenancy scan)"
                    Type     = 'Summary'
                    State    = 'Scanned'
                    IP       = 'N/A'
                    Shape    = 'N/A'
                    Region   = $configRegion
                    AD       = 'N/A'
                    Created  = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
                    Monitors = 0
                    Compute  = "$($computeDevices.Count) instances"
                    DBSystem = "$($dbDevices.Count) DB systems"
                    ADB      = "$($adbDevices.Count) autonomous DBs"
                    LB       = "$($lbDevices.Count) load balancers"
                    Tenancy  = $TenancyId
                }
            )
            Write-Host " No cloud resources found -- generating summary dashboard." -ForegroundColor Yellow
        }

        $dashPath = Join-Path $OutputDir 'OCI-Dashboard.html'

        if (Get-Command -Name 'Export-DynamicDashboardHtml' -ErrorAction SilentlyContinue) {
            Export-DynamicDashboardHtml -Data $dashboardRows `
                -OutputPath $dashPath `
                -ReportTitle 'OCI Discovery Dashboard' `
                -CardField 'Type','State' `
                -StatusField 'State'
        }
        else {
            Write-Warning "No dashboard function available. Exporting as JSON instead."
            $jsonPath = Join-Path $OutputDir "OCI-Plan-$(Get-Date -Format yyyyMMdd-HHmmss).json"
            $plan | Export-DiscoveryPlan -Format JSON -Path $jsonPath
            Write-Host "JSON exported: $jsonPath" -ForegroundColor Green
            break
        }

        Write-Host ""
        Write-Host "Dashboard generated: $dashPath" -ForegroundColor Green
        Write-Host " Devices: $($devicePlan.Count) | Monitors: $($dashboardRows.Count)" -ForegroundColor White

        $nmConsolePaths = @(
            "${env:ProgramFiles(x86)}\Ipswitch\WhatsUp\Html\NmConsole"
            "${env:ProgramFiles}\Ipswitch\WhatsUp\Html\NmConsole"
        )
        $nmConsolePath = $nmConsolePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
        if ($nmConsolePath) {
            $wugDashDir = Join-Path $nmConsolePath 'dashboards'
            if (-not (Test-Path $wugDashDir)) { New-Item -ItemType Directory -Path $wugDashDir -Force | Out-Null }
            $wugDashPath = Join-Path $wugDashDir 'OCI-Dashboard.html'
            try {
                Copy-Item -Path $dashPath -Destination $wugDashPath -Force
                Write-Host "Copied to WUG: $wugDashPath" -ForegroundColor Green
                Write-Host " Access via WUG web UI: /NmConsole/dashboards/OCI-Dashboard.html" -ForegroundColor Cyan
            }
            catch {
                Write-Warning "Could not copy to NmConsole (run as admin?): $_"
            }
            Deploy-DashboardWebConfig -Path $wugDashDir
        }
    }
    '6' {
        Write-Host "No action taken." -ForegroundColor Gray
    }
    default {
        Write-Host "Invalid choice." -ForegroundColor Red
    }
}
} # end foreach actionsToRun

Write-Host ""
Write-Host "Re-run anytime to discover new OCI resources." -ForegroundColor Cyan

# SIG # Begin signature block
# MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCMAxDFgs6NxIX9
# oOYPdRy1C9l0fD09d58313l1p5ZNfaCCJQ0wggVvMIIEV6ADAgECAhBI/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
# kPZPubdcMIIG7TCCBNWgAwIBAgIQCE/cM09+RU7bww+P+ZIYNTANBgkqhkiG9w0B
# AQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/
# BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg
# U0hBMjU2IDIwMjUgQ0ExMB4XDTI2MDgwNTAwMDAwMFoXDTM3MTEwNDIzNTk1OVow
# YzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQD
# EzJEaWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIw
# MjYgMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALZ7pvLJ/s1K+NSb
# TGWz/TjGMPh8CQ6RucZCLv5anHzWJjF/NWJrFIhy24fcpKXlgRiky4WAawDfU3YP
# 0BMxt9l3Dm5oCG5Z69AqEN1kgHg2epx+l+lZBcmJCcN0ASURML5uFIS80sZsDwO3
# BSkUxDjLJhBI+qiZP3aixAC/qEGLjsBNlLol9VZ7pfGEXiMlneJIC5/YKuizVzNF
# KZZEeoy/0B8Zm+nzKBgSWG52lCO1w+nCg6XpCtklTJXeIg283hw7TmmsZXR+SMbj
# brEOvZ3fP2VxIgeR28Y90ZStd3F9VuA5RVynb/whITPAo9b75Zr4Ta6Mj3URm26Q
# ZYMn/FnbuTegcoRcFEZ9FOqM5T6MTdtr/n74lIT/ug0eeOzmZ6QTFg33otX+bFRs
# IolvykE1jive4PuESaT8zzVeFWDAMDtozNgLctkGD1ZjkEyZtJrLl5ya0m5doH/S
# cpaZCZVl6pNUOCybMc/kxC6EAmSJY24L0yYKD1Nkddsnb/ItVKi/2nXpQNMu1PT5
# prW83vV8d67WowuUs0HdY4H8AMLGvdL/WHEj3ZnqMqAQQP9u3Ai9t+5eQ02GDwy0
# ODjdzi0xlp70W+ow63/0++YDEX1M0iwgUHwbrJvfpklkZQvw3+kv3vUPItdwrocz
# k9icflf55W1zOEKAcJVAIXpcMCU9AgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAA
# MB0GA1UdDgQWBBQUyWOKMC7USvtulPPm40B+9ezN4jAfBgNVHSMEGDAWgBTvb1NK
# 6eQGfHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYI
# KwYBBQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8v
# b2NzcC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZT
# SEEyNTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5k
# aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2
# U0hBMjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9
# bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQCNxTphHp1SCt+ZrAmAfn0oQLFr0mLywSLa
# DXQIENoyKqxrFbJblzCVP/pkXmwXOdrOpWygLzlT12os5ipDCy35RBCg2UMeApEt
# rfGhz45F4Wt4WGdNdIbRWt3YTYJmpR+b7lr4d7Uwn+H600u4D7RnOGf8Wj4UNgAd
# ZkfHhHv1mx9EVh71SJelcEN/oORSjXzdjfw1iZH9d8Nh/thn6hH23d+VsPAr6GAY
# yzSA02nXD1nYLI7Ijmiv+xLCiYC41DSFYL3GhTiy0PxpawPtGRyaBVGzq+UiTfM8
# pD7KVyF5aQyWP4KhVGUUTnmm/RlYJoW3TiXA/+t0YcT2oRVBm3JETjajHug2AL+v
# 5jhtKVnd3D0rbHXEu27o+Q8p4sEWPMqKDB+qbceb6T/6WcwTwXmQ9lOCLLYcsQeS
# WmvKqzpAec9etE14jOQAzLKWdE3w/TCaKtLRaRT7LCkRYVnhA2D73FLje1O5b3HR
# 5eHs0NzU/+xX7NbEdcofy0W3Wdwd1XOqtlpg/JgwtKfZM5dqO94lbUveOiJBI+xZ
# EbGRsMNbXmMREUTgu+Oca7Y73MPWcslIx2VhkSKSXjDbD6rgg39H5Mh7QfieAIjW
# agkJNt68Yfim6cjEzVSiLSeZfdkr5dtFPTW6jATlWJdYeeDRGCyatf8R1hSjzSvd
# N8yWQPT9gzGCBkQwggZAAgEBMGgwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1Nl
# Y3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWdu
# aW5nIENBIFIzNgIQB5zg5NEUf4XNOXPPdi036zANBglghkgBZQMEAgEFAKCBhDAY
# BgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3
# AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEi
# BCAKCbXVviuKV2q30KCPro6/QG7QmzICHSeWjR2WOqz6SDANBgkqhkiG9w0BAQEF
# AASCAgAICleU8GKXU9hPmDENYQK6Bdqlko7DJjuc88KTxCKryHdV+aSsCBSSu//h
# xHE3giBuwHAOGt4zlved3X0uMDeLi/PX3hS0b4IHgBTW3Gu37JEB6XIQdngbMz6j
# tvObghNxD0HBEQ7JbHb/vyFaPU4cwMwiMEa0Adtfl9mbQpYqqCcNSDAoBoHaeTm6
# WM2doZgm3vIzia/K2M56bibC1gD+9eSavmJPGANxop0PB8ZOTVtOXV2Fkc2N+Vor
# TeEDJuYdv2wXnohC90jqX33CQRaYxedHGg5NXgKvE2AhyBA2K7YOaU19pefACSri
# hA4PJ78mZmGfSwzs/gG1IDLJ1VrwExwzqqjkcmaPAP+q3j8e04AUReO3HUbTBMC8
# JqM51j8iPJc+M6ecZvY5lfRe59V7uBdmn8cVKdu6pUir6tW5MuBsTZUPSdjYsymD
# tcatb9pB1E7o+66nIVzeJKJ7lpYG2BdGf+y+NFVQhlBqxCLnOzZcnjiGfwmAaJ4u
# qrmByggt8zbsldlaAs1wzyTnWjpWOReU4vU48CpT/pTDYBaJkpx5DJU1XONnVRbi
# nUb1IqQyS36YqIdEDgNL5/YUb5KD3sUVitetdQQHwr4cmlovkhKiLxBjhO+gGVW4
# 3n3YoZZ/g413KY/c67yVzF92qJNCg4W75Szu5sl2lTK7Rd/X5aGCAyYwggMiBgkq
# hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E
# aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l
# U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAhP3DNPfkVO28MPj/mS
# GDUwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG
# CSqGSIb3DQEJBTEPFw0yNjA5MjkxOTUxMDJaMC8GCSqGSIb3DQEJBDEiBCCm9EpF
# vq8/aN5+iPOfsfNXHstH1e3inJYCrME7te2b/zANBgkqhkiG9w0BAQEFAASCAgCn
# iqCycpP+a4EtjcqRZAP2M1Tcrg/NYKXsUZrgajmSfoyHBCB9WVScNOQ/C0StKuDL
# b9AP6ybsTpwc3Z0hUgZjPXgrJB7B6ZQhrRkCbSG4I7gKdgTi8Hm0YzVcBXIji5Lo
# 9AalWPIIciwK98ePPeBlEnBUGqyKsbji8PpUmd8EgyWU1hXtnpKERjr6Z/VItIau
# qsCK4h+ly2DegH0xrVANhCyIWzZSQplO9CQX7p6LFwY1BYbCwni1b0FGf/htjEgO
# ET2BygnoWFUqGpYO+Fxj6Au7rWFIyIBVhv7kKa5vY9NGTIkqtMUJlzPO0h8WiCNy
# SdFiI74GH6QUALNI405BXWBttMyxn4V9PXQ/SVYgTDvABbTIcKp2LUE8QtPS+Iuu
# fANNbFDuXW5T98b/z7vO0UJKa5bXz9GZxeepsyEfeWUcu9u3FPmkCPS7OZiIWIjJ
# KleTiu2pVAFWzbpV+NiSEkd7mFsp2sEPNxXT23/l+KCdmizkFG0kBvj4X087mlbl
# QnkoI8FchdgWACpTcWvQV/G2wRrrj8pZNYD/U8/BtvejNxSIfcRNH7Cm0WTLC/dr
# Hg/LwwO8Qmbn2Ei384AEMpAq3Rb8ovNqP5174nrVZMS9X5ZXmIX3pN/BsgccW8UL
# SyE3la0kiWd/sJ9e3O2gPPMUQt7A0+qSAd3LRGMa7A==
# SIG # End signature block