Private/Export-WorkspaceTestHtmlReport.ps1
|
function Export-WorkspaceTestHtmlReport { <# .SYNOPSIS Writes an HTML validation report. .DESCRIPTION Creates a human-readable HTML report from WorkspaceTestingFramework result objects. Results are sorted by group, type, and name. .PARAMETER Results Result objects to include in the report. .PARAMETER Path Destination path for the HTML report. .OUTPUTS None. .NOTES Private report helper. #> [CmdletBinding()] param( [Parameter(Mandatory)] [object[]]$Results, [Parameter(Mandatory)] [string]$Path ) $Results = @($Results | Sort-Object -Property Group, Type, Name) $directory = Split-Path -Path $Path -Parent if ($directory -and -not (Test-Path -LiteralPath $directory)) { New-Item -Path $directory -ItemType Directory -Force | Out-Null } $metaKeys = [System.Collections.Generic.HashSet[string]]([System.StringComparer]::OrdinalIgnoreCase) foreach ($k in @('Name','Group','Tags','OnFailure','Severity','PassWhen','FailWhen')) { $metaKeys.Add($k) | Out-Null } function ConvertTo-JsonObj { param([object]$Value) if ($null -eq $Value) { return $null } $json = $Value | ConvertTo-Json -Depth 5 -Compress try { return $json | ConvertFrom-Json -ErrorAction Stop } catch { return $null } } function Format-CriteriaRules { param([object]$Rules) if ($null -eq $Rules) { return '' } $list = @($Rules) $lines = foreach ($rule in $list) { $parts = @($rule.PSObject.Properties | ForEach-Object { $v = if ($_.Value -is [array]) { $_.Value -join ' | ' } else { $_.Value } "$($_.Name) = $v" }) [System.Net.WebUtility]::HtmlEncode($parts -join '; ') } ($lines | ForEach-Object { "<div class=""criteria-rule"">$_</div>" }) -join '' } # Maps Actual field names to their Expected equivalent for alignment $keyAliases = @{ 'DisplayVersion' = 'Version' 'DisplayName' = 'Name' } function ConvertTo-HtmlDetailTable { param([object]$Expected, [object]$Actual, [string]$Status, [string]$OnFailure) $expObj = ConvertTo-JsonObj $Expected $actObj = ConvertTo-JsonObj $Actual # Normalise Actual keys using aliases so they align with Expected rows if ($actObj) { foreach ($alias in $keyAliases.GetEnumerator()) { $actProp = $actObj.PSObject.Properties[$alias.Key] if ($actProp -and -not $actObj.PSObject.Properties[$alias.Value]) { $actObj | Add-Member -NotePropertyName $alias.Value -NotePropertyValue $actProp.Value -Force $actObj.PSObject.Properties.Remove($alias.Key) } } } # Collect all meaningful keys from both sides, excluding meta $allKeys = [System.Collections.Generic.List[string]]::new() $seen = [System.Collections.Generic.HashSet[string]]([System.StringComparer]::OrdinalIgnoreCase) foreach ($src in @($expObj, $actObj)) { if ($null -eq $src) { continue } foreach ($p in $src.PSObject.Properties) { if (-not $metaKeys.Contains($p.Name) -and $seen.Add($p.Name)) { $allKeys.Add($p.Name) } } } if ($allKeys.Count -eq 0) { return '' } $rows = foreach ($key in $allKeys) { $expProp = if ($expObj) { $expObj.PSObject.Properties[$key] } else { $null } $actProp = if ($actObj) { $actObj.PSObject.Properties[$key] } else { $null } # $expProp present but null means "key exists, no constraint" → show (any) $expVal = if ($null -eq $expProp) { $null } elseif ($null -eq $expProp.Value) { '' } elseif ($expProp.Value -is [array]) { $expProp.Value -join ', ' } else { $expProp.Value.ToString() } $actVal = if ($null -eq $actProp) { $null } elseif ($null -eq $actProp.Value) { $null } elseif ($actProp.Value -is [array]) { $actProp.Value -join ', ' } else { $actProp.Value.ToString() } # Skip rows where both sides have nothing to show if ($null -eq $expVal -and $null -eq $actVal) { continue } if ([string]::IsNullOrEmpty($expVal) -and [string]::IsNullOrEmpty($actVal)) { continue } $differs = ($null -ne $expVal -and $expVal -ne '' -and $null -ne $actVal -and $expVal -ne $actVal) $rowClass = if ($differs -and $Status -ne 'Pass') { ' class="row-diff"' } else { '' } $kHtml = [System.Net.WebUtility]::HtmlEncode($key) # Empty string means key present but null value — show (any) to indicate no constraint $eHtml = if ($null -eq $expVal) { '<em class="na">—</em>' } elseif ($expVal -eq '') { '<em class="na">(any)</em>' } else { [System.Net.WebUtility]::HtmlEncode($expVal) } $aHtml = if ($null -ne $actVal) { [System.Net.WebUtility]::HtmlEncode($actVal) } else { '<em class="na">—</em>' } "<tr$rowClass><td class=""dk"">$kHtml</td><td>$eHtml</td><td>$aHtml</td></tr>" } # PassWhen / FailWhen criteria (services, scheduled tasks) $criteriaRows = '' if ($expObj) { $pw = $expObj.PSObject.Properties['PassWhen'] $fw = $expObj.PSObject.Properties['FailWhen'] if ($pw -or $fw) { $criteriaRows += '<tr class="criteria-header"><td colspan="3">Criteria</td></tr>' if ($pw) { $criteriaRows += "<tr><td class=""dk"">PassWhen</td><td colspan=""2"">$(Format-CriteriaRules $pw.Value)</td></tr>" } if ($fw) { $criteriaRows += "<tr><td class=""dk"">FailWhen</td><td colspan=""2"">$(Format-CriteriaRules $fw.Value)</td></tr>" } } } $onFailureRow = '' if ($OnFailure) { $of = [System.Net.WebUtility]::HtmlEncode($OnFailure) $onFailureRow = "<tr class=""criteria-header""><td colspan=""3"">Settings</td></tr><tr><td class=""dk"">On Failure</td><td colspan=""2"">$of</td></tr>" } $allRows = ($rows -join '') + $criteriaRows + $onFailureRow if (-not $allRows) { return '' } "<table class=""cmp""><thead><tr><th></th><th>Expected</th><th>Actual</th></tr></thead><tbody>$allRows</tbody></table>" } $groups = $Results | Group-Object -Property Group | Sort-Object -Property Name $sections = foreach ($group in $groups) { $displayName = if ([string]::IsNullOrWhiteSpace($group.Name)) { 'General' } else { $group.Name } $groupName = [System.Net.WebUtility]::HtmlEncode($displayName) $tableRows = foreach ($result in ($group.Group | Sort-Object Type, Name)) { $detail = ConvertTo-HtmlDetailTable -Expected $result.Expected -Actual $result.Actual -Status $result.Status -OnFailure $result.OnFailure $message = [System.Net.WebUtility]::HtmlEncode($result.Message) $name = [System.Net.WebUtility]::HtmlEncode($result.Name) $type = [System.Net.WebUtility]::HtmlEncode($result.Type) $status = [System.Net.WebUtility]::HtmlEncode($result.Status) $hasDetail = [bool]$detail $expandClass = if ($hasDetail) { ' expandable' } else { '' } $detailRow = if ($hasDetail) { "<tr class=""detail-row"" hidden><td colspan=""4""><div class=""detail-panel"">$detail</div></td></tr>" } else { '' } "<tr class=""status-$($result.Status)$expandClass""><td><span class=""badge badge-$($result.Status)"">$status</span></td><td>$type</td><td>$name</td><td>$message</td></tr>$detailRow" } @" <section class="group-section"> <h2 class="group-heading">$groupName</h2> <div class="table-wrap"> <table> <thead> <tr><th>Status</th><th>Type</th><th>Name</th><th>Message</th></tr> </thead> <tbody> $($tableRows -join [Environment]::NewLine) </tbody> </table> </div> </section> "@ } $summary = [PSCustomObject]@{ Total = @($Results).Count Pass = @($Results | Where-Object Status -eq 'Pass').Count Fail = @($Results | Where-Object Status -eq 'Fail').Count Warning = @($Results | Where-Object Status -eq 'Warning').Count Skipped = @($Results | Where-Object Status -eq 'Skipped').Count NotApplicable = @($Results | Where-Object Status -eq 'NotApplicable').Count BlockingFailures = @($Results | Where-Object { $_.Status -eq 'Fail' -and $_.OnFailure -eq 'Fail' }).Count } $html = @" <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Workspace Testing Framework Report</title> <style> *, *::before, *::after { box-sizing: border-box; } body { font-family: 'Segoe UI', system-ui, Arial, sans-serif; margin: 0; padding: 32px 40px 60px; background: #f0f2f5; color: #1f2328; } header { margin-bottom: 28px; } header h1 { margin: 0 0 4px; font-size: 22px; font-weight: 700; color: #0d1117; } header p { margin: 0; font-size: 13px; color: #57606a; } /* ── Summary cards ── */ .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; margin-bottom: 28px; } .metric { background: #fff; border-radius: 10px; padding: 16px 14px 12px; box-shadow: 0 1px 3px rgba(0,0,0,.08); border-top: 4px solid #d0d7de; } .metric strong { display: block; font-size: 28px; font-weight: 700; line-height: 1; margin-bottom: 4px; } .metric span { font-size: 12px; color: #57606a; text-transform: uppercase; letter-spacing: .04em; } .metric.m-pass { border-color: #1a7f37; } .metric.m-pass strong { color: #1a7f37; } .metric.m-fail { border-color: #cf222e; } .metric.m-fail strong { color: #cf222e; } .metric.m-warn { border-color: #bf8700; } .metric.m-warn strong { color: #bf8700; } .metric.m-skip { border-color: #57606a; } .metric.m-skip strong { color: #57606a; } .metric.m-block { border-color: #8250df; } .metric.m-block strong { color: #8250df; } /* ── Group sections ── */ .group-section { margin-bottom: 28px; } .group-heading { font-size: 15px; font-weight: 700; color: #0d1117; margin: 0 0 10px; padding: 0; } /* ── Table wrapper ── */ .table-wrap { background: #fff; border-radius: 10px; box-shadow: 0 1px 3px rgba(0,0,0,.08); overflow-x: auto; } table { border-collapse: collapse; width: 100%; font-size: 13px; } thead tr { background: #f6f8fa; } th { padding: 10px 12px; text-align: left; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: #57606a; border-bottom: 2px solid #d0d7de; white-space: nowrap; } td { padding: 9px 12px; border-bottom: 1px solid #eaeef2; vertical-align: top; } tbody tr:last-child td { border-bottom: none; } tbody tr:hover:not(.detail-row) { background: #f6f8fa; } /* ── Row left-border accent ── */ .status-Pass td:first-child { border-left: 3px solid #1a7f37; } .status-Fail td:first-child { border-left: 3px solid #cf222e; } .status-Warning td:first-child { border-left: 3px solid #bf8700; } .status-Skipped td:first-child { border-left: 3px solid #57606a; } .status-NotApplicable td:first-child { border-left: 3px solid #8250df; } /* ── Expandable rows ── */ tr.expandable { cursor: pointer; } tr.expandable td:last-child::after { content: ' ▸'; font-size: 10px; color: #57606a; float: right; margin-top: 2px; } tr.expandable.open td:last-child::after { content: ' ▾'; } tr.detail-row td { padding: 0; border-bottom: 1px solid #eaeef2; background: #f6f8fa; } .detail-panel { padding: 12px 16px 14px; } /* ── Comparison table ── */ table.cmp { border-collapse: collapse; font-size: 12px; width: auto; } table.cmp thead tr { background: #eaeef2; } table.cmp th { padding: 4px 8px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: #57606a; border-bottom: 1px solid #d0d7de; text-align: left; white-space: nowrap; } table.cmp td { padding: 3px 8px; border-bottom: 1px solid #eaeef2; vertical-align: top; white-space: nowrap; } table.cmp tr:last-child td { border-bottom: none; } td.dk { font-weight: 600; color: #57606a; white-space: nowrap; } tr.row-diff td:nth-child(2) { background: #fff8c5; } tr.row-diff td:nth-child(3) { background: #ffebe9; } tr.criteria-header td { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: #57606a; background: #eaeef2; padding: 4px 8px; border-top: 1px solid #d0d7de; } .criteria-rule { font-size: 12px; color: #1f2328; padding: 1px 0; } em.na { color: #adb5bd; font-style: normal; } /* ── Status badge ── */ .badge { display: inline-block; padding: 2px 9px; border-radius: 12px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; } .badge-Pass { background: #dafbe1; color: #116329; } .badge-Fail { background: #ffebe9; color: #a40e26; } .badge-Warning { background: #fff8c5; color: #7d4e00; } .badge-Skipped { background: #eaeef2; color: #424a53; } .badge-NotApplicable { background: #fbefff; color: #5a32a3; } /* ── Key/value sub-table for Expected / Actual ── */ table.kv { border-collapse: collapse; font-size: 12px; width: 100%; } table.kv td { padding: 2px 6px; border: none; border-bottom: 1px solid #eaeef2; vertical-align: top; word-break: break-all; } table.kv tr:last-child td { border-bottom: none; } td.dk { font-weight: 600; color: #57606a; white-space: nowrap; padding-right: 10px; } </style> </head> <body> <header> <h1>Workspace Testing Framework Report</h1> <p>Generated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")</p> </header> <section class="summary"> <div class="metric"> <strong>$($summary.Total)</strong><span>Total</span></div> <div class="metric m-pass"> <strong>$($summary.Pass)</strong><span>Pass</span></div> <div class="metric m-fail"> <strong>$($summary.Fail)</strong><span>Fail</span></div> <div class="metric m-warn"> <strong>$($summary.Warning)</strong><span>Warning</span></div> <div class="metric m-skip"> <strong>$($summary.Skipped)</strong><span>Skipped</span></div> <div class="metric m-block"> <strong>$($summary.BlockingFailures)</strong><span>Blocking</span></div> </section> $($sections -join [Environment]::NewLine) <script> document.querySelectorAll('tr.expandable').forEach(function(row) { row.addEventListener('click', function() { var detail = row.nextElementSibling; if (detail && detail.classList.contains('detail-row')) { var hidden = detail.hasAttribute('hidden'); if (hidden) { detail.removeAttribute('hidden'); row.classList.add('open'); } else { detail.setAttribute('hidden', ''); row.classList.remove('open'); } } }); }); </script> </body> </html> "@ Set-Content -LiteralPath $Path -Value $html -Encoding UTF8 } # SIG # Begin signature block # MIImdwYJKoZIhvcNAQcCoIImaDCCJmQCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD+u2udNEGRXPF+ # JvrPC/XJjM4frFeJvB9PHktsH7NLyqCCIAowggYUMIID/KADAgECAhB6I67aU2mW # D5HIPlz0x+M/MA0GCSqGSIb3DQEBDAUAMFcxCzAJBgNVBAYTAkdCMRgwFgYDVQQK # Ew9TZWN0aWdvIExpbWl0ZWQxLjAsBgNVBAMTJVNlY3RpZ28gUHVibGljIFRpbWUg # U3RhbXBpbmcgUm9vdCBSNDYwHhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1OTU5 # WjBVMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSwwKgYD # VQQDEyNTZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5nIENBIFIzNjCCAaIwDQYJ # KoZIhvcNAQEBBQADggGPADCCAYoCggGBAM2Y2ENBq26CK+z2M34mNOSJjNPvIhKA # VD7vJq+MDoGD46IiM+b83+3ecLvBhStSVjeYXIjfa3ajoW3cS3ElcJzkyZlBnwDE # JuHlzpbN4kMH2qRBVrjrGJgSlzzUqcGQBaCxpectRGhhnOSwcjPMI3G0hedv2eNm # GiUbD12OeORN0ADzdpsQ4dDi6M4YhoGE9cbY11XxM2AVZn0GiOUC9+XE0wI7CQKf # OUfigLDn7i/WeyxZ43XLj5GVo7LDBExSLnh+va8WxTlA+uBvq1KO8RSHUQLgzb1g # bL9Ihgzxmkdp2ZWNuLc+XyEmJNbD2OIIq/fWlwBp6KNL19zpHsODLIsgZ+WZ1AzC # s1HEK6VWrxmnKyJJg2Lv23DlEdZlQSGdF+z+Gyn9/CRezKe7WNyxRf4e4bwUtrYE # 2F5Q+05yDD68clwnweckKtxRaF0VzN/w76kOLIaFVhf5sMM/caEZLtOYqYadtn03 # 4ykSFaZuIBU9uCSrKRKTPJhWvXk4CllgrwIDAQABo4IBXDCCAVgwHwYDVR0jBBgw # FoAU9ndq3T/9ARP/FqFsggIv0Ao9FCUwHQYDVR0OBBYEFF9Y7UwxeqJhQo1SgLqz # YZcZojKbMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMBMGA1Ud # JQQMMAoGCCsGAQUFBwMIMBEGA1UdIAQKMAgwBgYEVR0gADBMBgNVHR8ERTBDMEGg # P6A9hjtodHRwOi8vY3JsLnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNUaW1lU3Rh # bXBpbmdSb290UjQ2LmNybDB8BggrBgEFBQcBAQRwMG4wRwYIKwYBBQUHMAKGO2h0 # dHA6Ly9jcnQuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY1RpbWVTdGFtcGluZ1Jv # b3RSNDYucDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTAN # BgkqhkiG9w0BAQwFAAOCAgEAEtd7IK0ONVgMnoEdJVj9TC1ndK/HYiYh9lVUacah # RoZ2W2hfiEOyQExnHk1jkvpIJzAMxmEc6ZvIyHI5UkPCbXKspioYMdbOnBWQUn73 # 3qMooBfIghpR/klUqNxx6/fDXqY0hSU1OSkkSivt51UlmJElUICZYBodzD3M/SFj # eCP59anwxs6hwj1mfvzG+b1coYGnqsSz2wSKr+nDO+Db8qNcTbJZRAiSazr7KyUJ # Go1c+MScGfG5QHV+bps8BX5Oyv9Ct36Y4Il6ajTqV2ifikkVtB3RNBUgwu/mSiSU # ice/Jp/q8BMk/gN8+0rNIE+QqU63JoVMCMPY2752LmESsRVVoypJVt8/N3qQ1c6F # ibbcRabo3azZkcIdWGVSAdoLgAIxEKBeNh9AQO1gQrnh1TA8ldXuJzPSuALOz1Uj # b0PCyNVkWk7hkhVHfcvBfI8NtgWQupiaAeNHe0pWSGH2opXZYKYG4Lbukg7HpNi/ # KqJhue2Keak6qH9A8CeEOB7Eob0Zf+fU+CCQaL0cJqlmnx9HCDxF+3BLbUufrV64 # EbTI40zqegPZdA+sXCmbcZy6okx/SjwsusWRItFA3DE8MORZeFb6BmzBtqKJ7l93 # 9bbKBy2jvxcJI98Va95Q5JnlKor3m0E7xpMeYRriWklUPsetMSf2NvUQa/E5vVye # fQIwggZFMIIELaADAgECAhAIMk+dt9qRb2Pk8qM8Xl1RMA0GCSqGSIb3DQEBCwUA # MFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMu # QS4xJDAiBgNVBAMTG0NlcnR1bSBDb2RlIFNpZ25pbmcgMjAyMSBDQTAeFw0yNDA0 # MDQxNDA0MjRaFw0yNzA0MDQxNDA0MjNaMGsxCzAJBgNVBAYTAk5MMRIwEAYDVQQH # DAlTY2hpam5kZWwxIzAhBgNVBAoMGkpvaG4gQmlsbGVrZW5zIENvbnN1bHRhbmN5 # MSMwIQYDVQQDDBpKb2huIEJpbGxla2VucyBDb25zdWx0YW5jeTCCAaIwDQYJKoZI # hvcNAQEBBQADggGPADCCAYoCggGBAMslntDbSQwHZXwFhmibivbnd0Qfn6sqe/6f # os3pKzKxEsR907RkDMet2x6RRg3eJkiIr3TFPwqBooyXXgK3zxxpyhGOcuIqyM9J # 28DVf4kUyZHsjGO/8HFjrr3K1hABNUszP0o7H3o6J31eqV1UmCXYhQlNoW9FOmRC # 1amlquBmh7w4EKYEytqdmdOBavAD5Xq4vLPxNP6kyA+B2YTtk/xM27TghtbwFGKn # u9Vwnm7dFcpLxans4ONt2OxDQOMA5NwgcUv/YTpjhq9qoz6ivG55NRJGNvUXsM3w # 2o7dR6Xh4MuEGrTSrOWGg2A5EcLH1XqQtkF5cZnAPM8W/9HUp8ggornWnFVQ9/6M # ga+ermy5wy5XrmQpN+x3u6tit7xlHk1Hc+4XY4a4ie3BPXG2PhJhmZAn4ebNSBwN # Hh8z7WTT9X9OFERepGSytZVeEP7hgyptSLcuhpwWeR4QdBb7dV++4p3PsAUQVHFp # wkSbrRTv4EiJ0Lcz9P1HPGFoHiFAQQIDAQABo4IBeDCCAXQwDAYDVR0TAQH/BAIw # ADA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY2NzY2EyMDIxLmNybC5jZXJ0dW0u # cGwvY2NzY2EyMDIxLmNybDBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0 # dHA6Ly9jY3NjYTIwMjEub2NzcC1jZXJ0dW0uY29tMDUGCCsGAQUFBzAChilodHRw # Oi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwvY2NzY2EyMDIxLmNlcjAfBgNVHSMEGDAW # gBTddF1MANt7n6B0yrFu9zzAMsBwzTAdBgNVHQ4EFgQUO6KtBpOBgmrlANVAnyiQ # C6W6lJwwSwYDVR0gBEQwQjAIBgZngQwBBAEwNgYLKoRoAYb2dwIFAQQwJzAlBggr # BgEFBQcCARYZaHR0cHM6Ly93d3cuY2VydHVtLnBsL0NQUzATBgNVHSUEDDAKBggr # BgEFBQcDAzAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBAEQsN8wg # PMdWVkwHPPTN+jKpdns5AKVFjcn00psf2NGVVgWWNQBIQc9lEuTBWb54IK6Ga3hx # QRZfnPNo5HGl73YLmFgdFQrFzZ1lnaMdIcyh8LTWv6+XNWfoyCM9wCp4zMIDPOs8 # LKSMQqA/wRgqiACWnOS4a6fyd5GUIAm4CuaptpFYr90l4Dn/wAdXOdY32UhgzmSu # xpUbhD8gVJUaBNVmQaRqeU8y49MxiVrUKJXde1BCrtR9awXbqembc7Nqvmi60tYK # lD27hlpKtj6eGPjkht0hHEsgzU0Fxw7ZJghYG2wXfpF2ziN893ak9Mi/1dmCNmor # GOnybKYfT6ff6YTCDDNkod4egcMZdOSv+/Qv+HAeIgEvrxE9QsGlzTwbRtbm6gwY # YcVBs/SsVUdBn/TSB35MMxRhHE5iC3aUTkDbceo/XP3uFhVL4g2JZHpFfCSu2TQr # rzRn2sn07jfMvzeHArCOJgBW1gPqR3WrJ4hUxL06Rbg1gs9tU5HGGz9KNQMfQFQ7 # 0Wz7UIhezGcFcRfkIfSkMmQYYpsc7rfzj+z0ThfDVzzJr2dMOFsMlfj1T6l22GBq # 9XQx0A4lcc5Fl9pRxbOuHHWFqIBD/BCEhwniOCySzqENd2N+oz8znKooSISStnkN # aYXt6xblJF2dx9Dn89FK7d1IquNxOwt0tI5dMIIGYjCCBMqgAwIBAgIRAKQpO24e # 3denNAiHrXpOtyQwDQYJKoZIhvcNAQEMBQAwVTELMAkGA1UEBhMCR0IxGDAWBgNV # BAoTD1NlY3RpZ28gTGltaXRlZDEsMCoGA1UEAxMjU2VjdGlnbyBQdWJsaWMgVGlt # ZSBTdGFtcGluZyBDQSBSMzYwHhcNMjUwMzI3MDAwMDAwWhcNMzYwMzIxMjM1OTU5 # WjByMQswCQYDVQQGEwJHQjEXMBUGA1UECBMOV2VzdCBZb3Jrc2hpcmUxGDAWBgNV # BAoTD1NlY3RpZ28gTGltaXRlZDEwMC4GA1UEAxMnU2VjdGlnbyBQdWJsaWMgVGlt # ZSBTdGFtcGluZyBTaWduZXIgUjM2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC # CgKCAgEA04SV9G6kU3jyPRBLeBIHPNyUgVNnYayfsGOyYEXrn3+SkDYTLs1crcw/ # ol2swE1TzB2aR/5JIjKNf75QBha2Ddj+4NEPKDxHEd4dEn7RTWMcTIfm492TW22I # 8LfH+A7Ehz0/safc6BbsNBzjHTt7FngNfhfJoYOrkugSaT8F0IzUh6VUwoHdYDpi # ln9dh0n0m545d5A5tJD92iFAIbKHQWGbCQNYplqpAFasHBn77OqW37P9BhOASdmj # p3IijYiFdcA0WQIe60vzvrk0HG+iVcwVZjz+t5OcXGTcxqOAzk1frDNZ1aw8nFhG # EvG0ktJQknnJZE3D40GofV7O8WzgaAnZmoUn4PCpvH36vD4XaAF2CjiPsJWiY/j2 # xLsJuqx3JtuI4akH0MmGzlBUylhXvdNVXcjAuIEcEQKtOBR9lU4wXQpISrbOT8ux # +96GzBq8TdbhoFcmYaOBZKlwPP7pOp5Mzx/UMhyBA93PQhiCdPfIVOCINsUY4U23 # p4KJ3F1HqP3H6Slw3lHACnLilGETXRg5X/Fp8G8qlG5Y+M49ZEGUp2bneRLZoyHT # yynHvFISpefhBCV0KdRZHPcuSL5OAGWnBjAlRtHvsMBrI3AAA0Tu1oGvPa/4yeei # Ayu+9y3SLC98gDVbySnXnkujjhIh+oaatsk/oyf5R2vcxHahajMCAwEAAaOCAY4w # ggGKMB8GA1UdIwQYMBaAFF9Y7UwxeqJhQo1SgLqzYZcZojKbMB0GA1UdDgQWBBSI # YYyhKjdkgShgoZsx0Iz9LALOTzAOBgNVHQ8BAf8EBAMCBsAwDAYDVR0TAQH/BAIw # ADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDBKBgNVHSAEQzBBMDUGDCsGAQQBsjEB # AgEDCDAlMCMGCCsGAQUFBwIBFhdodHRwczovL3NlY3RpZ28uY29tL0NQUzAIBgZn # gQwBBAIwSgYDVR0fBEMwQTA/oD2gO4Y5aHR0cDovL2NybC5zZWN0aWdvLmNvbS9T # ZWN0aWdvUHVibGljVGltZVN0YW1waW5nQ0FSMzYuY3JsMHoGCCsGAQUFBwEBBG4w # bDBFBggrBgEFBQcwAoY5aHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVi # bGljVGltZVN0YW1waW5nQ0FSMzYuY3J0MCMGCCsGAQUFBzABhhdodHRwOi8vb2Nz # cC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAYEAAoE+pIZyUSH5ZakuPVKK # 4eWbzEsTRJOEjbIu6r7vmzXXLpJx4FyGmcqnFZoa1dzx3JrUCrdG5b//LfAxOGy9 # Ph9JtrYChJaVHrusDh9NgYwiGDOhyyJ2zRy3+kdqhwtUlLCdNjFjakTSE+hkC9F5 # ty1uxOoQ2ZkfI5WM4WXA3ZHcNHB4V42zi7Jk3ktEnkSdViVxM6rduXW0jmmiu71Z # pBFZDh7Kdens+PQXPgMqvzodgQJEkxaION5XRCoBxAwWwiMm2thPDuZTzWp/gUFz # i7izCmEt4pE3Kf0MOt3ccgwn4Kl2FIcQaV55nkjv1gODcHcD9+ZVjYZoyKTVWb4V # qMQy/j8Q3aaYd/jOQ66Fhk3NWbg2tYl5jhQCuIsE55Vg4N0DUbEWvXJxtxQQaVR5 # xzhEI+BjJKzh3TQ026JxHhr2fuJ0mV68AluFr9qshgwS5SpN5FFtaSEnAwqZv3IS # +mlG50rK7W3qXbWwi4hmpylUfygtYLEdLQukNEX1jiOKMIIGgjCCBGqgAwIBAgIQ # NsKwvXwbOuejs902y8l1aDANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYD # VQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBS # U0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjEwMzIyMDAwMDAwWhcNMzgw # MTE4MjM1OTU5WjBXMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1p # dGVkMS4wLAYDVQQDEyVTZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5nIFJvb3Qg # UjQ2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAiJ3YuUVnnR3d6Lkm # gZpUVMB8SQWbzFoVD9mUEES0QUCBdxSZqdTkdizICFNeINCSJS+lV1ipnW5ihkQy # C0cRLWXUJzodqpnMRs46npiJPHrfLBOifjfhpdXJ2aHHsPHggGsCi7uE0awqKggE # /LkYw3sqaBia67h/3awoqNvGqiFRJ+OTWYmUCO2GAXsePHi+/JUNAax3kpqstbl3 # vcTdOGhtKShvZIvjwulRH87rbukNyHGWX5tNK/WABKf+Gnoi4cmisS7oSimgHUI0 # Wn/4elNd40BFdSZ1EwpuddZ+Wr7+Dfo0lcHflm/FDDrOJ3rWqauUP8hsokDoI7D/ # yUVI9DAE/WK3Jl3C4LKwIpn1mNzMyptRwsXKrop06m7NUNHdlTDEMovXAIDGAvYy # nPt5lutv8lZeI5w3MOlCybAZDpK3Dy1MKo+6aEtE9vtiTMzz/o2dYfdP0KWZwZIX # bYsTIlg1YIetCpi5s14qiXOpRsKqFKqav9R1R5vj3NgevsAsvxsAnI8Oa5s2oy25 # qhsoBIGo/zi6GpxFj+mOdh35Xn91y72J4RGOJEoqzEIbW3q0b2iPuWLA911cRxgY # 5SJYubvjay3nSMbBPPFsyl6mY4/WYucmyS9lo3l7jk27MAe145GWxK4O3m3gEFEI # kv7kRmefDR7Oe2T1HxAnICQvr9sCAwEAAaOCARYwggESMB8GA1UdIwQYMBaAFFN5 # v1qqK0rPVIDh2JvAnfKyA2bLMB0GA1UdDgQWBBT2d2rdP/0BE/8WoWyCAi/QCj0U # JTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zATBgNVHSUEDDAKBggr # BgEFBQcDCDARBgNVHSAECjAIMAYGBFUdIAAwUAYDVR0fBEkwRzBFoEOgQYY/aHR0 # cDovL2NybC51c2VydHJ1c3QuY29tL1VTRVJUcnVzdFJTQUNlcnRpZmljYXRpb25B # dXRob3JpdHkuY3JsMDUGCCsGAQUFBwEBBCkwJzAlBggrBgEFBQcwAYYZaHR0cDov # L29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEADr5lQe1oRLjl # ocXUEYfktzsljOt+2sgXke3Y8UPEooU5y39rAARaAdAxUeiX1ktLJ3+lgxtoLQhn # 5cFb3GF2SSZRX8ptQ6IvuD3wz/LNHKpQ5nX8hjsDLRhsyeIiJsms9yAWnvdYOdEM # q1W61KE9JlBkB20XBee6JaXx4UBErc+YuoSb1SxVf7nkNtUjPfcxuFtrQdRMRi/f # InV/AobE8Gw/8yBMQKKaHt5eia8ybT8Y/Ffa6HAJyz9gvEOcF1VWXG8OMeM7Vy7B # s6mSIkYeYtddU1ux1dQLbEGur18ut97wgGwDiGinCwKPyFO7ApcmVJOtlw9FVJxw # /mL1TbyBns4zOgkaXFnnfzg4qbSvnrwyj1NiurMp4pmAWjR+Pb/SIduPnmFzbSN/ # G8reZCL4fvGlvPFk4Uab/JVCSmj59+/mB2Gn6G/UYOy8k60mKcmaAZsEVkhOFuoj # 4we8CYyaR9vd9PGZKSinaZIkvVjbH/3nlLb0a7SBIkiRzfPfS9T+JesylbHa1LtR # V9U/7m0q7Ma2CQ/t392ioOssXW7oKLdOmMBl14suVFBmbzrt5V5cQPnwtd3UOTpS # 9oCG+ZZheiIvPgkDmA8FzPsnfXW5qHELB43ET7HHFHeRPRYrMBKjkb8/IN7Po0d0 # hQoF4TeMM+zYAJzoKQnVKOLg8pZVPT8wgga5MIIEoaADAgECAhEAmaOACiZVO2Wr # 3G6EprPqOTANBgkqhkiG9w0BAQwFADCBgDELMAkGA1UEBhMCUEwxIjAgBgNVBAoT # GVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0 # aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0 # d29yayBDQSAyMB4XDTIxMDUxOTA1MzIxOFoXDTM2MDUxODA1MzIxOFowVjELMAkG # A1UEBhMCUEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEkMCIG # A1UEAxMbQ2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENBMIICIjANBgkqhkiG9w0B # AQEFAAOCAg8AMIICCgKCAgEAnSPPBDAjO8FGLOczcz5jXXp1ur5cTbq96y34vuTm # flN4mSAfgLKTvggv24/rWiVGzGxT9YEASVMw1Aj8ewTS4IndU8s7VS5+djSoMcbv # IKck6+hI1shsylP4JyLvmxwLHtSworV9wmjhNd627h27a8RdrT1PH9ud0IF+njvM # k2xqbNTIPsnWtw3E7DmDoUmDQiYi/ucJ42fcHqBkbbxYDB7SYOouu9Tj1yHIohzu # C8KNqfcYf7Z4/iZgkBJ+UFNDcc6zokZ2uJIxWgPWXMEmhu1gMXgv8aGUsRdaCtVD # 2bSlbfsq7BiqljjaCun+RJgTgFRCtsuAEw0pG9+FA+yQN9n/kZtMLK+Wo837Q4QO # ZgYqVWQ4x6cM7/G0yswg1ElLlJj6NYKLw9EcBXE7TF3HybZtYvj9lDV2nT8mFSkc # SkAExzd4prHwYjUXTeZIlVXqj+eaYqoMTpMrfh5MCAOIG5knN4Q/JHuurfTI5XDY # O962WZayx7ACFf5ydJpoEowSP07YaBiQ8nXpDkNrUA9g7qf/rCkKbWpQ5boufUnq # 1UiYPIAHlezf4muJqxqIns/kqld6JVX8cixbd6PzkDpwZo4SlADaCi2JSplKShBS # ND36E/ENVv8urPS0yOnpG4tIoBGxVCARPCg1BnyMJ4rBJAcOSnAWd18Jx5n858JS # qPECAwEAAaOCAVUwggFRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFN10XUwA # 23ufoHTKsW73PMAywHDNMB8GA1UdIwQYMBaAFLahVDkCw6A/joq8+tT4HKbROg79 # MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzAwBgNVHR8EKTAn # MCWgI6Ahhh9odHRwOi8vY3JsLmNlcnR1bS5wbC9jdG5jYTIuY3JsMGwGCCsGAQUF # BwEBBGAwXjAoBggrBgEFBQcwAYYcaHR0cDovL3N1YmNhLm9jc3AtY2VydHVtLmNv # bTAyBggrBgEFBQcwAoYmaHR0cDovL3JlcG9zaXRvcnkuY2VydHVtLnBsL2N0bmNh # Mi5jZXIwOQYDVR0gBDIwMDAuBgRVHSAAMCYwJAYIKwYBBQUHAgEWGGh0dHA6Ly93 # d3cuY2VydHVtLnBsL0NQUzANBgkqhkiG9w0BAQwFAAOCAgEAdYhYD+WPUCiaU58Q # 7EP89DttyZqGYn2XRDhJkL6P+/T0IPZyxfxiXumYlARMgwRzLRUStJl490L94C9L # GF3vjzzH8Jq3iR74BRlkO18J3zIdmCKQa5LyZ48IfICJTZVJeChDUyuQy6rGDxLU # UAsO0eqeLNhLVsgw6/zOfImNlARKn1FP7o0fTbj8ipNGxHBIutiRsWrhWM2f8pXd # d3x2mbJCKKtl2s42g9KUJHEIiLni9ByoqIUul4GblLQigO0ugh7bWRLDm0CdY9rN # LqyA3ahe8WlxVWkxyrQLjH8ItI17RdySaYayX3PhRSC4Am1/7mATwZWwSD+B7eMc # ZNhpn8zJ+6MTyE6YoEBSRVrs0zFFIHUR08Wk0ikSf+lIe5Iv6RY3/bFAEloMU+vU # BfSouCReZwSLo8WdrDlPXtR0gicDnytO7eZ5827NS2x7gCBibESYkOh1/w1tVxTp # V2Na3PR7nxYVlPu1JPoRZCbH86gc96UTvuWiOruWmyOEMLOGGniR+x+zPF/2DaGg # K2W1eEJfo2qyrBNPvF7wuAyQfiFXLwvWHamoYtPZo0LHuH8X3n9C+xN4YaNjt2yw # zOr+tKyEVAotnyU9vyEVOaIYMk3IeBrmFnn0gbKeTTyYeEEUz/Qwt4HOUBCrW602 # NCmvO1nm+/80nLy5r0AZvCQxaQ4xggXDMIIFvwIBATBqMFYxCzAJBgNVBAYTAlBM # MSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0Nl # cnR1bSBDb2RlIFNpZ25pbmcgMjAyMSBDQQIQCDJPnbfakW9j5PKjPF5dUTANBglg # hkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3 # DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEV # MC8GCSqGSIb3DQEJBDEiBCCcVT7ukYhgik+NMWUWRNVuKUz+Ne2AL8vDqxqtEB9W # hjANBgkqhkiG9w0BAQEFAASCAYCncydihNOeCMlra+qEr4ifwU7sZJa8hCj0jt33 # xI5LX6iRK7+B0ozNuF+LpWwLXLp233NIUbtQGtumdeLR3vYQVgdaDoF+0YhxP1CU # CIAKHeomMoTqzqCorJiTrAg4dObl1yTUZqVG66RDqVMxSZhcp3mEJ+MfywBmAYLA # h46cEpO/7q8k3l74g0BkRDYnynhwNkQO16+/U0SRsEGUbFrW15lAfQodouZzPw/W # u2008Jbduno9QL3a+9oNXQkR0tP+r9Sf9vc6MJOEZUFkg5L1pxi9eQjsABFhSZDE # fy/B9+x15zwmutbaLRZe/XiicHP7/nlDtNvDajGeS0RfL/Om04Nfd2gdCQlLx8lN # j9qo2gdKQT6Vt8AOWobVe6FEBMkL/+k0qDwe36L+/1UucS9YIoKayxolzn4bJE4r # y2QBbXPzR7zXIvOGmWSk2L3NnQTCvQwd23XP1F6n//H66GSHbnGOewcuUYGpWLBc # sx0S0D6R7abhBfgVA5Aqek85El2hggMjMIIDHwYJKoZIhvcNAQkGMYIDEDCCAwwC # AQEwajBVMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSww # KgYDVQQDEyNTZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5nIENBIFIzNgIRAKQp # O24e3denNAiHrXpOtyQwDQYJYIZIAWUDBAICBQCgeTAYBgkqhkiG9w0BCQMxCwYJ # KoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yNjA2MTIwOTQyMTJaMD8GCSqGSIb3 # DQEJBDEyBDAKpSmW1zgC09MdSBS/ARWp/MycLWl2RzBZjx+RnGbMv54OU0BX9k/6 # 67TKOzeItWQwDQYJKoZIhvcNAQEBBQAEggIAClhl8NMBC/6M7OvkXY5r1rAUdrLX # vKUpmr3uOvzBkeDsf8bNc0u9im7t02HYrc7/5B4BsT4SytrGb/RObZdS74yPVnPZ # p5TYXHltLILouRP0m9Vm4Q+SIE6JWDUtkqeeHeRmjVCJEbYIATxVCe2G7SY2ShHc # i4YIP+BI+3eqrWnii8vcxLQCN7T8R/qRUiyMcTHrVEEA5WNWAwwWU/OHZA23vggX # AC5b3WMSr33UvK5/Ub1sxFx7QqWjeGPuJ5j5IPJOIi/3/Mr+R/vyFyGZa2baNq7T # EcLlmOuKifH6lYF7AjXs6RETciJL5arzl1BMAa7o99lRenPO1AOqGEHAucVG6VYt # hffIvSf+j88nqfml1KDpFakRSipZNWjuF3+SKzQHEgs865Wcfl3/vdUrOtPU8J6p # VgLGSzOWpd3hqrj68qV7onlPeXzYsEal5XkmKx6iG5Lo6MYirEqofVaWNVWvQvAH # knSfNxsi++Am32QlywKj+ic7DD43bJJw1UWYSnvcaXVCe7/u0bvEwDXJ5wqwmDT/ # jBZSaIXhcgRJzxnM4PF5nDQmKDmXvD6NfXvaX6+LgeOwf2Y7Nx0ynCE97Rpc32q4 # v8onNEiMRg6hF4T9NqT3HFkfz60NLvZyIU1J9jHnt57QAoF0arFIC20kzY3uBhzI # OkVy4UpASlm+OCA= # SIG # End signature block |