ВАЖЛИВО Цю статтю, яка містить цей зразок сценарію, закрито. Починаючи з оновлень Windows, випущених 12 травня 2026 р., зразок сценарію розташовано в папці %systemroot%\SecureBoot\ExampleRolloutScripts на пристрої.

Скопіюйте та вставте цей зразок сценарію та змініть його за потреби для свого середовища:

<# . ПІДСУМОК     Агрегує дані JSON стану безпечного завантаження з кількох пристроїв у зведені звіти.

. ОПИС     Читає зібрані файли JSON стану безпечного завантаження та створює:     - Приладна дошка HTML із діаграмами та фільтрами     - Зведення за довірчим рельєфом     - Унікальний аналіз блоку пристрою для тестування стратегії          Підтримує:     - Файли для кожного комп'ютера: HOSTNAME_latest.json (рекомендовано)     - Один файл      JSON     Автоматично дедуплікує ім'я хоста, зберігаючи останню версію CollectionTime.     За замовчуванням включає лише пристрої з довірою "Action Req" або "High"     , щоб зосередитися на активних блоках. Використовуйте функцію -IncludeAllConfidenceLevels для перевизначення.

. PARAMETER InputPath     Шлях до файлів JSON:     - Папка: читає всі *_latest.json файли (або *.json, якщо _latest файлів немає)     - Файл: Читає один файл JSON

. Parameter OutputPath     Шлях до створених звітів (за промовчанням: .\SecureBootReports)

. ПРИКЛАД     # Агрегація з папки файлів на комп'ютері (рекомендовано)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$"     # Читає: \\contoso\SecureBootLogs$\*_latest.json

. ПРИКЛАД     # Користувацьке розташування виводу     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -OutputPath "C:\Reports\SecureBoot"

. ПРИКЛАД     # Включити тільки action Req і Висока впевненість (поведінка за замовчуванням)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$"     # Виключає: спостереження, призупинено, не підтримується

. ПРИКЛАД     # Включити всі довірчі рівні (заміщення фільтра)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeAllConfidenceLevels

. ПРИКЛАД     # Настроюваний фільтр довірчого рівня     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeConfidenceLevels @("Запит на змінення", "Високий", "Спостереження")

. ПРИКЛАД     # ENTERPRISE SCALE: Incremental mode - only process changed files (fast subsequent runs)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode     # Перший запуск: повне завантаження ~2 години для пристроїв із 500K     # Наступні запуски: секунди, якщо немає змін, хвилини для дельти

. ПРИКЛАД     # Пропустити HTML, якщо нічого не змінилося (найшвидше для моніторингу)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode -SkipReportIfUnchanged     # Якщо файли не змінювалися після останнього запуску: ~5 секунд

. ПРИКЛАД     # Режим лише зведення - пропуск великих таблиць пристроїв (1-2 хвилини проти 20 + хвилин)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -SummaryOnly     # Створює CSVs, але пропускає HTML-приладну дошку з повними таблицями пристроїв

. НОТАТКИ     З'єднайте Detect-SecureBootCertUpdateStatus.ps1 для розгортання підприємства.Повний посібник із розгортання див. в статті GPO-DEPLOYMENT-GUIDE.md.     Поведінка за промовчанням виключає пристрої "Спостереження", "Призупинено" та "Не підтримується"     щоб зосередити звітування лише на активних блоках пристроїв.#>

param(     [Parameter(Mandatory = $true)]     [string]$InputPath,          [Parameter(Mandatory = $false)]     [string]$OutputPath = ".\SecureBootReports",          [Parameter(Mandatory = $false)]     [string]$ScanHistoryPath = ".\SecureBootReports\ScanHistory.json",          [Parameter(Mandatory = $false)]     [string]$RolloutStatePath, # шлях до RolloutState.json для ідентифікації пристроїв      InProgress     [Parameter(Mandatory = $false)]     [string]$RolloutSummaryPath, # шлях до SecureBootRolloutSummary.json з Orchestrator (містить дані проекції)          [Parameter(Mandatory = $false)]     [string[]]$IncludeConfidenceLevels = @("Потрібна дія","Висока довірча впевненість"); # Включають лише ці довірчі рівні (за замовчуванням: лише активні блоки)          [Parameter(Mandatory = $false)]     [switch]$IncludeAllConfidenceLevels, # Перевизначити фільтр, щоб включити всі довірчі рівні          [Parameter(Mandatory = $false)]     [switch]$SkipHistoryTracking,          [Parameter(Mandatory = $false)]     [switch]$IncrementalMode, # Enable delta processing - only load changed files since last run          [Parameter(Mandatory = $false)]     [string]$CachePath, # Шлях до каталогу кеша (за замовчуванням: OutputPath\.cache)          [Parameter(Mandatory = $false)]     [int]$ParallelThreads = 8, # Кількість паралельних потоків для завантаження файлу (PS7+)          [Parameter(Mandatory = $false)]     [switch]$ForceFullRefresh, # Force full reload even incremental mode          [Parameter(Mandatory = $false)]     [switch]$SkipReportIfUnchanged, # Skip HTML/CSV generation if no files changed (just output stats)          [Parameter(Mandatory = $false)]     [switch]$SummaryOnly, # Generate summary stats only (no large device tables) - набагато швидше          [Parameter(Mandatory = $false)]     [switch]$StreamingMode # Memory-efficient mode: process chunks, write CSVs incrementally, keep only summaries in memory )

#Self-repair: Strip invisible Unicode characters injected by web CMS when copy-pasting from HTML articles.# support.microsoft.com CMS вводить пробіли нульової ширини (U+200B), нерозривні простори (U+00A0) та інші # невидимі символи навколо HTML-тегів у цьому рядку, що спричиняє помилки аналізу PowerShell.якщо ($MyInvocation.MyCommand.Path) {     $rawScript = [System.IO.File]::ReadAllText($MyInvocation.MyCommand.Path)     якщо ($rawScript -match '[\u200B-\u200F\uFEFF]' -або $rawScript -match '\xA0') {         Write-Host "УВАГА! Виявлено невидимі символи Юнікоду (імовірно, з веб-копіювання-вставлення) - сценарій автоочищення..." -ForegroundColor Yellow         $cleaned = $rawScript -replace '[\u200B-\u200F\uFEFF]', ''         $cleaned = $cleaned -replace '\xA0', ' '         [System.IO.File]::WriteAllText($MyInvocation.MyCommand.Path, $cleaned, [System.Text.UTF8Encoding]::new($false))         Write-Host "Сценарій очищено. Повторне запуск..." -ForegroundColor Green         & $MyInvocation.MyCommand.Path @PSBoundParameters         вийти з $LASTEXITCODE     } (}) }

# Автоматичне підвищення до PowerShell 7, якщо доступно (6x швидше для великих наборів даних) якщо ($PSVersionTable.PSVersion.Major -lt 7) {     $pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object –розгорнути джереловироботи     якщо ($pwshPath) {         Write-Host виявлено "PowerShell $($PSVersionTable.PSVersion) - повторне запуск з PowerShell 7 для швидшої обробки..." -ForegroundColor Yellow         # Перебудувати список аргументів зі зв'язаних параметрів         $relaunchArgs = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $MyInvocation.MyCommand.Path)         foreach ($key in $PSBoundParameters.Keys) {             $val = $PSBoundParameters[$key]             якщо ($val -is [switch]) {                 якщо ($val. IsPresent) { $relaunchArgs += "-$key" }             } elseif ($val -is [масив]) {                 $relaunchArgs += "-$key"                 $relaunchArgs += ($val -join ',')             } інакше {                 $relaunchArgs += "-$key"                 $relaunchArgs += "$val"             } (})         } (})         & $pwshPath @relaunchArgs         вийти $LASTEXITCODE     } (}) }

$ErrorActionPreference = "Продовжити" $timestamp = Get-Date -Format "yyyyMDd-HHmmss" $scanTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Зразки розгортання та моніторингу"

# Примітка. Цей сценарій не має залежностей від інших сценаріїв.# Щоб отримати повний набір інструментів, завантажте з: $DownloadUrl -> $DownloadSubPage

настроювання #region Write-Host "=" * 60 -ForegroundColor Cyan Write-Host "Агрегація даних безпечного завантаження" -ForegroundColor Cyan Write-Host "=" * 60 -ForegroundColor Cyan

# Створення каталогу виводу якщо (-not (test-path $OutputPath)) {     New-Item -ItemType Directory – шлях $OutputPath -Force | Out-Null }

# Завантаження даних – підтримує формати CSV (застарілі) і JSON (власні) Write-Host "'nЗавантажити дані з: $InputPath" -ForegroundColor Yellow

# Helper function to normalize device object (handle field name differences) функція Normalize-DeviceRecord {     param($device)          # Handle Hostname vs HostName (JSON використовує ім'я хоста, CSV використовує hostname)     якщо ($device. PSObject.Properties['Hostname'] -and -not $device. PSObject.Properties['HostName']) {         $device | Add-Member -NotePropertyName "HostName" -NotePropertyValue $device. Hostname –Force (Ім'я хоста)     } (})          # Handle Confidence vs ConfidenceLevel (JSON використовує Confidence, CSV використовує ConfidenceLevel)     # ConfidenceLevel – це офіційне ім'я поля – карта Впевненість у ньому     якщо ($device. PSObject.Properties['Confidence'] -and -not $device. PSObject.Properties['ConfidenceLevel']) {         $device | Add-Member -NotePropertyName "ConfidenceLevel" - NotePropertyValue $device. Впевненість - Сила     } (})          # Відстежувати стан оновлення за допомогою event1808Count OR UEFICA2023Status="Оновлено"     # Це дає змогу відстежувати кількість пристроїв у кожному довірчому блоці було оновлено     $event 1808 = 0     якщо ($device. PSObject.Properties['Event1808Count']) {         $event 1808 = [int]$device. Кількість подій 1808     } (})     $uefiCaUpdated = $false     якщо ($device. PSObject.Properties['UEFICA2023Status'] -and $device. UEFICA2023Status – eq "Оновлено") {         $uefiCaUpdated = $true     } (})          якщо ($event 1808 -gt 0 -або $uefiCaUpdated) {         # Позначити як оновлений для прикладної дошки або логіки розгортання , але не заміняти ConfidenceLevel         $device | Add-Member -NotePropertyName "IsUpdated" -NotePropertyValue $true -Force     } інакше {         $device | Add-Member -NotePropertyName "IsUpdated" -NotePropertyValue $false -Force                  # Класифікація ConfidenceLevel:         # - "Висока впевненість", "Під спостереженням...", "Тимчасово призупинено...", "Не підтримується..." = використання як є         # - Усе інше (null, пусто, "UpdateType:...", "Невідомо", "N/A") = припадає на дію, необхідну для лічильників         # Нормалізація не потрібна — гілка іншого лічильника потокового передавання обробляє його     } (})          # Handle OEMManufacturerName vs WMI_Manufacturer (JSON використовує OEM*, застаріле використання WMI_*)     якщо ($device. PSObject.Properties['OEMManufacturerName'] -and -not $device. PSObject.Properties['WMI_Manufacturer']) {         $device | Add-Member -NotePropertyName "WMI_Manufacturer" -NotePropertyValue $device. OEMManufacturerName –Force     } (})          # Handle OEMModelNumber vs WMI_Model     якщо ($device. PSObject.Properties['OEMModelNumber'] -and -not $device. PSObject.Properties['WMI_Model']) {         $device | Add-Member -NotePropertyName "WMI_Model" -NotePropertyValue $device. OEMModelNumber – сила     } (})          # Handle FirmwareVersion vs BIOSDescription     якщо ($device. PSObject.Properties['FirmwareVersion'] -and -not $device. PSObject.Properties['BIOSDescription']) {         $device | Add-Member -NotePropertyName "BIOSDescription" -NotePropertyValue $device. FirmwareVersion –Force     } (})          повернути $device }

#region інкрементна обробка та керування кешем # Налаштування шляхів кеша якщо (-не $CachePath) {     $CachePath = Join-Path $OutputPath ".cache" } (}) $manifestPath = Join-Path $CachePath "FileManifest.json" $deviceCachePath = Join-Path $CachePath "DeviceCache.json"

# Функції керування кешем функція Get-FileManifest {     param([рядок]$Path)     якщо ($Path "Тестовий шлях") {         спробуйте {             $json = Get-Content $Path -Raw | Перетворити файл із формату Json             # Перетворити PSObject на геш-таблицю (сумісна з PS5.1 – PS7 має -AsHashtable)             $ht = @{}             $json. PSObject.Properties | ForEach-Object { $ht[$_. Ім'я] = $_. Значення }             повернути $ht         } зловити {             повернути @{}         } (})     } (})     повернути @{} }

функція Save-FileManifest {     param([hashtable]$Manifest; [string]$Path)     $dir = Split-Path $Path -Parent     якщо (-not (test-path $dir)) {         New-Item -ItemType Directory – шлях $dir -Force | Out-Null     } (})     $Manifest | ConvertTo-Json -Глибина 3 -Стиснути | Set-Content $Path -Force }

функція Get-DeviceCache {     param([string]$Path)     якщо ($Path тестового шляху) {         спробуйте {             $cacheData = Get-Content $Path -Raw | Перетворити файл із формату Json             Write-Host " Loaded device cache: $($cacheData.Count) devices" -ForegroundColor DarkGray             повернути $cacheData         } зловити {             Write-Host "Кеш пошкоджено, буде перебудовано" -ForegroundColor Yellow             return @()         } (})     } (})     return @() }

функція Save-DeviceCache {     param($Devices;[рядок]$Path)     $dir = Split-Path $Path -Parent     якщо (-not (test-path $dir)) {         New-Item -ItemType Directory – шлях $dir -Force | Out-Null     } (})     # Перетворити на масив і зберегти     $deviceArray = @($Devices)     $deviceArray | ConvertTo-Json -Глибина 10 -Стиснути | Set-Content $Path -Force     Write-Host " Кеш збережених пристроїв: $($deviceArray.Count) devices" -ForegroundColor DarkGray }

функція Get-ChangedFiles {     param(         [System.IO.FileInfo[]$AllFiles,         [hashtable]$Manifest     )          $changed = [System.Collections.ArrayList]::new()     $unchanged = [System.Collections.ArrayList]::new()     $newManifest = @{}          # Створення нечутливого до регістра підстановки з маніфесту (нормалізація в нижньому регістрі)     $manifestLookup = @{}     foreach ($mk in $Manifest.Keys) {         $manifestLookup[$mk. ToLowerInvariant()] = $Manifest[$mk]     } (})          foreach ($file in $AllFiles) {         $key = $file. FullName.ToLowerInvariant() # Normalize path to lowercase         $lwt = $file. LastWriteTimeUtc.ToString("o")         $newManifest[$key] = @{             LastWriteTimeUtc = $lwt             Розмір = $file. Довжина         } (})                  якщо ($manifestLookup.ContainsKey($key)) {             $cached = $manifestLookup[$key]             якщо ($cached. LastWriteTimeUtc – eq $lwt –і $cached. Розмір –eq $file. Довжина) {                 [void]$unchanged. Add($file)                 Продовжити             } (})         } (})         [void]$changed. Add($file)     } (})          повернути @{         Змінено = $changed         Незмінний = $unchanged         NewManifest = $newManifest     } (}) }

# Надшвидкий паралельний файл завантаження за допомогою пакетної обробки функція Load-FilesParallel {     param(         [System.IO.FileInfo[]$Files,         [int]$Threads = 8     )

    $totalFiles = $Files. Розраховувати     # Використовуйте пакети з ~1000 файлів кожен для кращого контролю пам'яті     $batchSize = [math]::Min(1000; [math]::Ceiling($totalFiles / [math]::Max(1; $Threads)))     $batches = [System.Collections.Generic.List[object]]::new()     

    для ($i = 0; $i -lt $totalFiles; $i += $batchSize) {         $end = [math]::Min($i + $batchSize, $totalFiles)         $batch = $Files[$i.. ($end-1)]         $batches. Add($batch)     } (})     Write-Host " ($($batches. Кількість) пакетів ~$batchSize файлів кожен)" -NoNewline -ForegroundColor DarkGray     $flatResults = [System.Collections.Generic.List[object]::new()     # Перевірте, чи доступна паралельна оболонка PowerShell 7+     $canParallel = $PSVersionTable.PSVersion.Major -ge 7     якщо ($canParallel -and $Threads -gt 1) {         # PS7+: обробка пакетів паралельно         $results = $batches | ForEach-Object -ThrottleLimit $Threads -Parallel {             $batchFiles = $_             $batchResults = [System.Collections.Generic.List[object]]::new()             foreach ($file in $batchFiles) {                 спробуйте {                     $content = [System.IO.File]::ReadAllText($file. Повне ім'я) | Перетворити файл із формату Json                     $batchResults.Add($content)                 } зловити { }             } (})             $batchResults.ToArray()         } (})         foreach ($batch in $results) {             якщо ($batch) { foreach ($item in $batch) { $flatResults.Add($item) } } }         } (})     } інакше {         # PS5.1 резервний варіант: послідовна обробка (ще швидка для файлів <10K)         foreach ($file in $Files) {             спробуйте {                 $content = [System.IO.File]::ReadAllText($file. Повне ім'я) | Перетворити файл із формату Json                 $flatResults.Add($content)             } зловити { }         } (})     } (})     повернути $flatResults.ToArray() } (}) #endregion                         

$allDevices = @() якщо (test-path $InputPath -PathType Leaf) {     # Один файл JSON     якщо ($InputPath -like "*.json") {         $jsonContent = Get-Content -Path $InputPath -Raw | Перетворити файл із формату Json         $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ }         Write-Host "Завантажено записи $($allDevices.Count) із файлу"     } інакше {         Write-Error "Підтримується лише формат JSON. Файл має містити .json розширення".         вихід 1     } (}) } elseif (test-path $InputPath -PathType Container) {     # Папка – лише JSON     $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue |                    Where-Object { $_. Ім'я -notmatch "ScanHistory|Розгорнути стан|Розгортання плану" })          # Надавайте перевагу *_latest.json файлів, якщо вони існують (у режимі кожного комп'ютера)     $latestJson = $jsonFiles | Where-Object { $_. Ім'я –як "*_latest.json" }     якщо ($latestJson.Count -gt 0) { $jsonFiles = $latestJson }          $totalFiles = $jsonFiles.Count          якщо ($totalFiles -eq 0) {         Write-Error "У: $InputPath не знайдено файли JSON"         вихід 1     } (})          Write-Host "Знайдено $totalFiles файли JSON" -ForegroundColor Gray          # Helper function to match confidence levels (handles both short and full forms)     # Визначено раніше, щоб його могли використовувати як StreamingMode, так і звичайні шляхи     функція Test-ConfidenceLevel {         param([string]$Value; [string]$Match)         якщо ([рядок]::IsNullOrEmpty($Value)) { return $false }         перемикач ($Match) {             "HighConfidence" { return $Value -eq "High Confidence" }             "UnderObservation" { return $Value -like "Under Observation*" }             "ActionRequired" { return ($Value -like "*Дія потрібна*" -або $Value -eq "Потрібна дія") }             "TemporarilyPaused" { return $Value -like "Temporarily Paused*" }             "Не підтримується" { повернення ($Value -like "Не підтримується*" -або $Value -eq "Не підтримується") }             за замовчуванням { return $false }         } (})     } (})          #region режимі потокового передавання – ефективна обробка пам'яті для великих наборів даних     # Завжди використовуйте StreamingMode для ефективної обробки пам'яті та приладної дошки нового стилю     якщо (-не $StreamingMode) {         Write-Host "Auto-enabling StreamingMode (приладна дошка нового стилю)" -ForegroundColor Yellow         $StreamingMode = $true         якщо (-не $IncrementalMode) { $IncrementalMode = $true }     } (})          # Якщо параметр -StreamingMode увімкнуто, обробляти файли в фрагментах, зберігаючи в пам'яті лише лічильники.# Дані на рівні пристрою записуються до файлів JSON на блок для завантаження на вимогу на приладній дошці.# Використання пам'яті: ~1,5 ГБ незалежно від розміру набору даних (проти 10–20 ГБ без потокового передавання).якщо ($StreamingMode) {         Write-Host "УВІМКНУТО РЕЖИМ ПОТОКОВОГО ПЕРЕДАВАННЯ – ефективна обробка пам'яті" -ForegroundColor Green         $streamSw = [System.Diagnostics.Stopwatch]::StartNew()         # INCREMENTAL CHECK: If no files changed since last run, skip processing entirely         якщо ($IncrementalMode -and -not $ForceFullRefresh) {             $stManifestDir = Join-Path $OutputPath ".cache"             $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json"             якщо ($stManifestPath тестового шляху) {                 Write-Host "Перевірка змін після останнього потокового запуску..." -ForegroundColor Cyan                 $stOldManifest = Get-FileManifest -path $stManifestPath                 якщо ($stOldManifest.Count -gt 0) {                     $stChanged = $false                     # Швидка перевірка: та ж кількість файлів?                     якщо ($stOldManifest.Count -eq $totalFiles) {                         # Перевірте 100 найновіших файлів (відсортовані за спаданням LastWriteTime)                         # Якщо будь-який файл змінено, він матиме останню позначку часу та з'явиться першим                         $sampleSize = [math]::Min(100; $totalFiles)                         $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc – за спаданням | Select-Object –перший $sampleSize                         foreach ($sf in $sampleFiles) {                             $sfKey = $sf. FullName.ToLowerInvariant()                             якщо (-не $stOldManifest.ContainsKey($sfKey)) {                                 $stChanged = $true                                 Перерва                             } (})                             # Compare timestamps - cached may be DateTime or string after JSON roundtrip                             $cachedLWT = $stOldManifest[$sfKey]. LastWriteTimeUtc                             $fileDT = $sf. LastWriteTimeUtc                             спробуйте {                                 # Якщо кешовано вже dateTime (auto-converts ConvertFrom-Json), використовуйте безпосередньо                                 якщо ($cachedLWT -це [Дата й час]) {                                     $cachedDT = $cachedLWT.ToUniversalTime()                                 } інакше {                                     $cachedDT = [DateTimeOffset]::P arse("$cachedLWT"). Час за utcDate                                 } (})                                 if ([math]::Abs(($cachedDT – $fileDT). Усьогосекунд) -gt 1) {                                     $stChanged = $true                                     Перерва                                 } (})                             } зловити {                                 $stChanged = $true                                 Перерва                             } (})                         } (})                     } інакше {                         $stChanged = $true                     } (})                     якщо (-не $stChanged) {                         # Перевірте, чи існують вихідні файли                         $stSummaryExists = Get-ChildItem (join-path $OutputPath "SecureBoot_Summary_*.csv") -EA SilentlyContinue | Select-Object –Перший 1                         $stDashExists = Get-ChildItem (join-path $OutputPath "SecureBoot_Dashboard_*.html") -EA SilentlyContinue | Select-Object –Перший 1                         якщо ($stSummaryExists -та $stDashExists) {                             Write-Host " Не виявлено жодних змін ($totalFiles файлів без змін) – пропуск обробки" -ForegroundColor Green                             Write-Host " Остання приладна дошка: $($stDashExists.FullName)" -ForegroundColor White                             $cachedStats = Get-Content $stSummaryExists.FullName | Перетворення файлу CSV                             Write-Host " Пристрої: $($cachedStats.TotalDevices) | Оновлено: $($cachedStats.Оновлено) | Помилки: $($cachedStats.WithErrors)" -ForegroundColor Gray                             Write-Host " Завершено в $([math]::Round($streamSw.Elapsed.TotalSeconds, 1))s (обробка не потрібна)" -ForegroundColor Green                             повернути $cachedStats                         } (})                     } інакше {                         # DELTA PATCH: Find exactly which files changed                         Write-Host " Виявлено зміни – визначення змінених файлів..." -ForegroundColor Yellow                         $changedFiles = [System.Collections.ArrayList]::new()                         $newFiles = [System.Collections.ArrayList]::new()                         foreach ($jf in $jsonFiles) {                             $jfKey = $jf. FullName.ToLowerInvariant()                             якщо (-не $stOldManifest.ContainsKey($jfKey)) {                                 [void]$newFiles.Add($jf)                             } інакше {                                 $cachedLWT = $stOldManifest[$jfKey]. LastWriteTimeUtc                                 $fileDT = $jf. LastWriteTimeUtc                                 спробуйте {                                     $cachedDT = якщо ($cachedLWT -це [Дата й час]) { $cachedLWT.ToUniversalTime() } інакше { [DateTimeOffset]::P arse("$cachedLWT"). Час utcDate }                                     if ([math]::Abs(($cachedDT – $fileDT). TotalSeconds) -gt 1) { [void]$changedFiles.Add($jf) }                                 } улов { [void]$changedFiles.Add($jf) }                             } (})                         } (})                         $totalChanged = $changedFiles.Count + $newFiles.Count                         $changePct = [math]::Round(($totalChanged / $totalFiles) * 100, 1)                         Write-Host " Змінено: $($changedFiles.Count) | Створити: $($newFiles.Count) | Всього: $totalChanged ($changePct%)" -ForegroundColor Yellow                         якщо ($totalChanged -gt 0 -and $changePct -lt 10) {                             # DELTA PATCH MODE: <10% змінено, виправлено наявні дані                             Write-Host " Режим виправлення відмінностей ($changePct% < 10%) - виправлення файлів $totalChanged..." -ForegroundColor Green                             $dataDir = Join-Path $OutputPath "дані"                             # Завантажувати змінені/нові записи пристроїв                             $deltaDevices = @{}                             $allDeltaFiles = @($changedFiles) + @($newFiles)                             foreach ($df in $allDeltaFiles) {                                 спробуйте {                                     $devData = Get-Content $df. FullName -Raw | Перетворити файл із формату Json                                     $dev = Normalize-DeviceRecord $devData                                     якщо ($dev. HostName) { $deltaDevices[$dev. HostName] = $dev }                                 } зловити { }                             } (})                             Write-Host " Loaded $($deltaDevices.Count) changed device records" -ForegroundColor Gray                             # Для кожної категорії JSON: видалення старих записів для змінених імен хостів, додавання нових записів                             $categoryFiles = @("помилки", "known_issues", "missing_kek", "not_updated",                                 "task_disabled", "temp_failures", "perm_failures", "updated_devices",                                 "action_required", "secureboot_off", "rollout_inprogress")                             $changedHostnames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)                             foreach ($hn in $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) }                             foreach ($cat in $categoryFiles) {                                 $catPath = Join-Path $dataDir "$cat.json"                                 якщо ($catPath тестовий шлях) {                                     спробуйте {                                         $catData = Get-Content $catPath -Raw | Перетворити файл із формату Json                                         # Видалення старих записів для змінених імен хостів                                         $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. HostName) })                                         # Перекласти кожен змінений пристрій на категорії                                         # (буде додано нижче після класифікації)                                         $catData | ConvertTo-Json -Глибина 5 | Set-Content $catPath -Кодування UTF8                                     } зловити { }                                 } (})                             } (})                             # Класифікувати кожен змінений пристрій і додавати до файлів потрібної категорії                             foreach ($dev in $deltaDevices.Values) {                                 $slim = [ordered]@{                                     HostName = $dev. Ім'я хоста                                     WMI_Manufacturer = якщо ($dev. PSObject.Properties['WMI_Manufacturer']) { $dev. WMI_Manufacturer } інакше { "" }                                     WMI_Model = якщо ($dev. PSObject.Properties['WMI_Model']) { $dev. WMI_Model } інакше { "" }                                     BucketId = якщо ($dev. PSObject.Properties['BucketId']) { $dev. BucketId } інакше { "" }                                     ConfidenceLevel = if ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } інакше { "" }                                     IsUpdated = $dev. IsUpdated (Оновлення)                                     UEFICA2023Error = якщо ($dev. PSObject.Properties['UEFICA2023Error']) { $dev. UEFICA2023Error } інакше { $null }                                     SecureBootTaskStatus = якщо ($dev. PSObject.Properties['SecureBootTaskStatus']) { $dev. SecureBootTaskStatus } інакше { "" }                                     KnownIssueId = if ($dev. PSObject.Properties['KnownIssueId']) { $dev. KnownIssueId } інакше { $null }                                     SkipReasonKnownIssue = if ($dev. PSObject.Properties['SkipReasonKnownIssue']) { $dev. SkipReasonKnownIssue } інакше { $null }                                 } (})                                 $isUpd = $dev. IsUpdated – eq $true                                 $conf = якщо ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } інакше { "" }                                 $hasErr = (-not [рядок]::IsNullOrEmpty($dev. UEFICA2023Error) – і $dev. UEFICA2023Error -ne "0" -and $dev. UEFICA2023Error -ne "")                                 $tskDis = ($dev. SecureBootTaskEnabled – eq $false -або $dev. SecureBootTaskStatus – eq "Disabled" -або $dev. SecureBootTaskStatus – eq "NotFound")                                 $tskNF = ($dev. SecureBootTaskStatus – eq "NotFound")                                 $sbOn = ($dev. SecureBootEnabled – ne $false -and "$($dev. SecureBootEnabled)" -ne "False")                                 $e 1801 = якщо ($dev. PSObject.Properties['Event1801Count']) { [int]$dev. Подія1801Count } ще { 0 }                                 $e 1808 = якщо ($dev. PSObject.Properties['Event1808Count']) { [int]$dev. Подія1808Count } ще { 0 }                                 $e 1803 = якщо ($dev. PSObject.Properties['Event1803Count']) { [int]$dev. Подія1803Count } ще { 0 }                                 $mKEK = ($e 1803 -gt 0 -або $dev. MissingKEK –eq $true)                                 $hKI = ((-not [рядок]::IsNullOrEmpty($dev. SkipReasonKnownIssue)) - або (-not [рядок]::IsNullOrEmpty($dev. Відомі ідентифікатори)))                                 $rStat = якщо ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } інакше { "" }                                 # Додати до відповідних файлів категорій                                 $targets = @()                                 якщо ($isUpd) { $targets += "updated_devices" }                                 якщо ($hasErr) { $targets += "помилки" }                                 якщо ($hKI) { $targets += "known_issues" }                                 якщо ($mKEK) { $targets += "missing_kek" }                                 якщо (-не $isUpd -та $sbOn) { $targets += "not_updated" }                                 якщо ($tskDis) { $targets += "task_disabled" }                                 якщо (-не $isUpd -and ($tskDis -або (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $targets += "temp_failures" }                                 якщо (-не $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -або ($tskNF -and $hasErr))) { $targets += "perm_failures" }                                 якщо (-не $isUpd -and (Test-ConfidenceLevel $conf 'ActionRequired')) { $targets += "action_required" }                                 якщо (-не $sbOn) { $targets += "secureboot_off" }                                 якщо ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $targets += "rollout_inprogress" }                                 foreach ($tgt in $targets) {                                     $tgtPath = Join-Path $dataDir "$tgt.json"                                     якщо (тестовий $tgtPath) {                                         $existing = Get-Content $tgtPath -Raw | Перетворити файл із формату Json                                         $existing = @($existing) + @([PSCustomObject]$slim)                                         $existing | ConvertTo-Json -Глибина 5 | Set-Content $tgtPath -кодування UTF8                                     } (})                                 } (})                             } (})                             # Повторне створення CSVs із виправлених JSON                             Write-Host " Відновлення CSV з виправлених даних..." -ForegroundColor Gray                             $newTimestamp = Get-Date -Format "yyyyMDd-HHmmss"                             foreach ($cat in $categoryFiles) {                                 $catJsonPath = Join-Path $dataDir "$cat.json"                                 $catCsvPath = Join-Path $OutputPath "SecureBoot_${cat}_$newTimestamp.csv"                                 якщо ($catJsonPath "Тестовий шлях") {                                     спробуйте {                                         $catJsonData = Get-Content $catJsonPath -Raw | Перетворити файл із формату Json                                         якщо ($catJsonData.Count -gt 0) {                                             $catJsonData | Export-Csv -Path $catCsvPath -NoTypeInformation -Encoding UTF8                                         } (})                                     } зловити { }                                 } (})                             } (})                             # Статистика перерахунку з виправлених файлів JSON                             Write-Host " Переобчислення зведення з виправлених даних..." -ForegroundColor Gray                             $patchedStats = [ordered]@{ ReportGeneratedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss") }                             $pTotal = 0; $pUpdated = 0; $pErrors = 0; $pKI = 0; $pKEK = 0                             $pTaskDis = 0; $pTempFail = 0; $pPermFail = 0; $pActionReq = 0; $pSBOff = 0; $pRIP = 0                             foreach ($cat in $categoryFiles) {                                 $catPath = Join-Path $dataDir "$cat.json"                                 $cnt = 0                                 якщо ($catPath test-path) { спробуйте { $cnt = (Get-Content $catPath -Raw | Перетворення Зром-Озону). Кількість } улов { } }                                 перемикач ($cat) {                                     "updated_devices" { $pUpdated = $cnt }                                     "помилки" { $pErrors = $cnt }                                     "known_issues" { $pKI = $cnt }                                     "missing_kek" { $pKEK = $cnt }                                     Обчислюваний обчислень: "not_updated" { } #                                     "task_disabled" { $pTaskDis = $cnt }                                     "temp_failures" { $pTempFail = $cnt }                                     "perm_failures" { $pPermFail = $cnt }                                     "action_required" { $pActionReq = $cnt }                                     "secureboot_off" { $pSBOff = $cnt }                                     "rollout_inprogress" { $pRIP = $cnt }                                 } (})                             } (})                             $pNotUpdated = (Get-Content (Join-Path $dataDir "not_updated.json") -Raw | Перетворення Зром-Озону). Розраховувати                             $pTotal = $pUpdated + $pNotUpdated + $pSBOff                             Write-Host "Дельта патч завершено: $totalChanged пристрої оновлено" -ForegroundColor Green                             Write-Host " Усього: $pTotal | Оновлено: $pUpdated | Не оновлено: $pNotUpdated | Помилки: $pErrors" -ForegroundColor White                             # Оновити маніфест                             $stManifestDir = Join-Path $OutputPath ".cache"                             $stNewManifest = @{}                             foreach ($jf in $jsonFiles) {                                 $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{                                     LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o"); Розмір = $jf. Довжина                                 } (})                             } (})                             Save-FileManifest – $stNewManifest маніфесту – шлях $stManifestPath                             Write-Host " Завершено в $([math]::Round($streamSw.Elapsed.TotalSeconds, 1))s (виправлення відмінностей – $totalChanged пристрої)" -ForegroundColor Green                             # Переробити повне потокове передавання, щоб повторно створити приладну дошку HTML                             # Файли даних уже виправлено, тому приладна дошка залишатиметься поточною                             Write-Host " Відновлення приладної дошки з виправлених даних..." -ForegroundColor Yellow                         } інакше {                             Write-Host " $changePct% змінених файлів (>= 10%) – потрібна повна обробка потокового передавання" -ForegroundColor Yellow                         } (})                     } (})                 } (})             } (})         } (})         # Створення підкаталогу даних для JSON-файлів на пристрої на вимогу         $dataDir = Join-Path $OutputPath "дані"         if (-not (test-path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null }         # Deduplication via HashSet (O(1) per lookup, ~50MB для 600K імен хостів)         $seenHostnames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)         # Полегшені підсумкові лічильники (замінює $allDevices + $uniqueDevices в пам'яті)         $c = @{             Всього = 0; SBEnabled = 0; SBOff = 0             Оновлено = 0; HighConf = 0; UnderObs = 0; ActionReq = 0; TempPaused = 0; NotSupported = 0; NoConfData = 0             TaskDisabled = 0; TaskNotFound = 0; TaskDisabledNotUpdated = 0             З помилками = 0; InProgress = 0; NotYetInitiated = 0; RolloutInProgress = 0             WithKnownIssues = 0; WithMissingKEK = 0; TempFailures = 0; PermFailures = 0; NeedsReboot = 0             Очікування оновлення = 0         } (})         # Відстеження блоку для AtRisk/SafeList (легкі набори)         $stFailedBuckets = [System.Collections.Generic.HashSet[string]]::new()         $stSuccessBuckets = [System.Collections.Generic.HashSet[string]]::new()         $stAllBuckets = @{}         $stMfrCounts = @{}         $stErrorCodeCounts = @{}; $stErrorCodeSamples = @{}         $stKnownIssueCounts = @{}         # Файли даних пристрою в груповому режимі: накопичувати блок, очищати за межами блока         $stDeviceFiles = @("помилки", "known_issues", "missing_kek", "not_updated"             "task_disabled", "temp_failures", "perm_failures", "updated_devices", "action_required"             "secureboot_off", "rollout_inprogress", "under_observation", "needs_reboot", "update_pending")         $stDeviceFilePaths = @{}; $stDeviceFileCounts = @{}         foreach ($dfName in $stDeviceFiles) {             $dfPath = Join-Path $dataDir "$dfName.json"             [System.IO.File]::WriteAllText($dfPath, "['n", [System.Text.Encoding]::UTF8)             $stDeviceFilePaths[$dfName] = $dfPath; $stDeviceFileCounts[$dfName] = 0         } (})         # Slim device record for JSON output (only essential fields, ~200 bytes vs ~2KB full)         функція Get-SlimDevice {             param($Dev)             повернення [замовлено]@{                 HostName = $Dev.HostName                 WMI_Manufacturer = якщо ($Dev.PSObject.Properties['WMI_Manufacturer']) { $Dev.WMI_Manufacturer } інакше { "" }                 WMI_Model = якщо ($Dev.PSObject.Properties['WMI_Model']) { $Dev.WMI_Model } інакше { "" }                 BucketId = якщо ($Dev.PSObject.Properties['BucketId']) { $Dev.BucketId } інакше { "" }                 ConfidenceLevel = if ($Dev.PSObject.Properties['ConfidenceLevel']) { $Dev.ConfidenceLevel } інакше { "" }                 IsUpdated = $Dev.IsUpdated                 UEFICA2023Error = if ($Dev.PSObject.Properties['UEFICA2023Error']) { $Dev.UEFICA2023Error } інакше { $null }                 SecureBootTaskStatus = if ($Dev.PSObject.Properties['SecureBootTaskStatus']) { $Dev.SecureBootTaskStatus } інакше { "" }                 KnownIssueId = if ($Dev.PSObject.Properties['KnownIssueId']) { $Dev.KnownIssueId } інакше { $null }                 SkipReasonKnownIssue = if ($Dev.PSObject.Properties['SkipReasonKnownIssue']) { $Dev.SkipReasonKnownIssue } інакше { $null }                 UEFICA2023Status = if ($Dev.PSObject.Properties['UEFICA2023Status']) { $Dev.UEFICA2023Status } інакше { $null }                 AvailableUpdatesPolicy = if ($Dev.PSObject.Properties['AvailableUpdatesPolicy']) { $Dev.AvailableUpdatesPolicy } інакше { $null }                 WinCSKeyApplied = if ($Dev.PSObject.Properties['WinCSKeyApplied']) { $Dev.WinCSKeyApplied } інакше { $null }             } (})         } (})         # Очищення пакета до файлу JSON (режим додавання)         функція Flush-DeviceBatch {             param([string]$StreamName; [System.Collections.Generic.List[object]$Batch)             якщо ($Batch.Count -eq 0) { return }             $fPath = $stDeviceFilePaths[$StreamName]             $fSb = [System.Text.StringBuilder]::new()             foreach ($fDev in $Batch) {                 якщо ($stDeviceFileCounts[$StreamName] -gt 0) { [void]$fSb.Append(",'n") }                 [void]$fSb.Append(($fDev | ConvertTo-Json -Compress))                 $stDeviceFileCounts[$StreamName]++             } (})             [System.IO.File]::AppendAllText($fPath, $fSb.ToString(), [System.Text.Encoding]::UTF8)         } (})         # ОСНОВНИЙ ЦИКЛ ПОТОКОВОГО ПЕРЕДАВАННЯ         $stChunkSize = якщо ($totalFiles -le 10000) { $totalFiles } інакше { 10000 }         $stTotalChunks = [math]::Ceiling($totalFiles / $stChunkSize)         $stPeakMemMB = 0         якщо ($stTotalChunks -gt 1) {             Write-Host "Обробка файлів $totalFiles в $stTotalChunks фрагментах $stChunkSize (потокове передавання, $ParallelThreads потоки):" -ForegroundColor Cyan         } інакше {             Write-Host "Обробка файлів $totalFiles (потокове передавання, потоки $ParallelThreads):" -ForegroundColor Cyan         } (})         для ($ci = 0; $ci -lt $stTotalChunks; $ci++) {             $cStart = $ci * $stChunkSize             $cEnd = [math]::Min($cStart + $stChunkSize, $totalFiles) - 1             $cFiles = $jsonFiles[$cStart. $cEnd]             якщо ($stTotalChunks -gt 1) {                 Write-Host " Блок $($ci + 1)/$stTotalChunks ($($cFiles.Count) файли): " -NoNewline -ForegroundColor Gray             } інакше {                 Write-Host " Завантаження файлів $($cFiles.Count): " -NoNewline -ForegroundColor Gray             } (})             $cSw = [System.Diagnostics.Stopwatch]::StartNew()             $rawDevices = Load-FilesParallel -Files $cFiles -потоки $ParallelThreads             # Для пакетних списків блоків             $cBatches = @{}             foreach ($df in $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]::new() }             $cNew = 0; $cDupe = 0             foreach ($raw in $rawDevices) {                 якщо (-не $raw) { continue }                 $device = Normalize-DeviceRecord $raw                 $hostname = $device. Ім'я хоста                 якщо (-не $hostname) { continue }                 якщо ($seenHostnames.Contains($hostname)) { $cDupe++; продовжити }                 [void]$seenHostnames.Add($hostname)                 $cNew++; $c.Total++                 $sbOn = ($device. SecureBootEnabled – ne $false -and "$($device. SecureBootEnabled)" -ne "False")                 якщо ($sbOn) { $c.SBEnabled++ } інакше { $c.SBOff++; $cBatches["secureboot_off"]. Add((Get-SlimDevice $device)) }                 $isUpd = $device. IsUpdated – eq $true                 $conf = якщо ($device. PSObject.Properties['ConfidenceLevel'] -and $device. ConfidenceLevel) { "$($device. ConfidenceLevel)" } інакше { "" }                 $hasErr = (-not [рядок]::IsNullOrEmpty($device. UEFICA2023Error) - і "$($device. UEFICA2023Error)" -ne "0" -and "$($device. UEFICA2023Error)" -ne "")                 $tskDis = ($device. SecureBootTaskEnabled – eq $false -або "$($device. SecureBootTaskStatus)" -eq "Disabled" -або "$($device. SecureBootTaskStatus)" -eq "NotFound")                 $tskNF = ("$($device. SecureBootTaskStatus)" -eq "NotFound")                 $bid = якщо ($device. PSObject.Properties['BucketId'] –і $device. BucketId) { "$($device. BucketId)" } інакше { "" }                 $e 1808 = якщо ($device. PSObject.Properties['Event1808Count']) { [int]$device. Подія1808Count } ще { 0 }                 $e 1801 = якщо ($device. PSObject.Properties['Event1801Count']) { [int]$device. Подія1801Count } ще { 0 }                 $e 1803 = якщо ($device. PSObject.Properties['Event1803Count']) { [int]$device. Подія1803Count } ще { 0 }                 $mKEK = ($e 1803 -gt 0 -або $device. MissingKEK – eq $true -або "$($device. MissingKEK)" -eq "True")                 $hKI = ((-not [рядок]::IsNullOrEmpty($device. SkipReasonKnownIssue)) - або (-not [рядок]::IsNullOrEmpty($device. Відомі ідентифікатори)))                 $rStat = якщо ($device. PSObject.Properties['RolloutStatus']) { $device. RolloutStatus } інакше { "" }                 $mfr = якщо ($device. PSObject.Properties['WMI_Manufacturer'] -and -not [string]::IsNullOrEmpty($device. WMI_Manufacturer)) { $device. WMI_Manufacturer } інакше { "Невідомо" }                 $bid = якщо (-not [рядок]::IsNullOrEmpty($bid)) { $bid } інакше { "" }                 # Попередня позначка очікування оновлення (застосовано політику/WinCS, стан ще не оновлено, SB ON, завдання не вимкнуто)                 $uefiStatus = якщо ($device. PSObject.Properties['UEFICA2023Status']) { "$($device. UEFICA2023Status)" } інакше { "" }                 $hasPolicy = ($device. PSObject.Properties['AvailableUpdatesPolicy'] -and $null -ne $device. AvailableUpdatesPolicy – і "$($device. AvailableUpdatesPolicy)" -ne '')                 $hasWinCS = ($device. PSObject.Properties['WinCSKeyApplied'] -and $device. WinCSKeyApplied –eq $true)                 $statusPending = ([рядок]::IsNullOrEmpty($uefiStatus) - або $uefiStatus -eq "NotStarted" -або $uefiStatus -eq "InProgress")                 $isUpdatePending = (($hasPolicy -або $hasWinCS) - і $statusPending -and -not $isUpd -and $sbOn -and -not $tskDis)                 якщо ($isUpd) {                     $c.Оновлено++; [void]$stSuccessBuckets.Add($bid); $cBatches["updated_devices"]. Add((Get-SlimDevice $device))                     # Track Updated devices that need reboot (UEFICA2023Status=Updated but Event1808=0)                     якщо ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) }                 } (})                 elseif (-not $sbOn) {                     # SecureBoot OFF – поза сферою, не класифікуйте за довірою                 } (})                 інакше {                     якщо ($isUpdatePending) { } # враховується окремо в очікуванні оновлення – взаємовиключний для секторної діаграми                     elseif (Test-ConfidenceLevel $conf "HighConfidence") { $c.HighConf++ }                     elseif (Test-ConfidenceLevel $conf "UnderObservation") { $c.UnderObs++ }                     elseif (Test-ConfidenceLevel $conf "TemporarilyPaused") { $c.TempPaused++ }                     elseif (Test-ConfidenceLevel $conf "NotSupported") { $c.NotSupported++ }                     інакше { $c.ActionReq++ }                     якщо ([рядок]::IsNullOrEmpty($conf)) { $c.NoConfData++ }                 } (})                 якщо ($tskDis) { $c.TaskDisabled++; $cBatches["task_disabled"]. Add((Get-SlimDevice $device)) }                 якщо ($tskNF) { $c.TaskNotFound++ }                 якщо (-не $isUpd -та $tskDis) { $c.TaskDisabledNotUpdated++ }                 якщо ($hasErr) {                     $c.WithErrors++; [void]$stFailedBuckets.Add($bid); $cBatches["помилки"]. Add((Get-SlimDevice $device))                     $ec = $device. UEFICA2023Error                     якщо (-не $stErrorCodeCounts.ContainsKey($ec)) { $stErrorCodeCounts[$ec] = 0; $stErrorCodeSamples[$ec] = @() }                     $stErrorCodeCounts[$ec]++                     якщо ($stErrorCodeSamples[$ec]. Кількість -lt 5) { $stErrorCodeSamples[$ec] += $hostname }                 } (})                 якщо ($hKI) {                     $c.WithKnownIssues++; $cBatches["known_issues"]. Add((Get-SlimDevice $device))                     $ki = якщо (-not [рядок]::IsNullOrEmpty($device. SkipReasonKnownIssue)) { $device. SkipReasonKnownIssue } інакше { $device. Відомі ідентифікатори _ ysueId }                     якщо (-not $stKnownIssueCounts.ContainsKey($ki)) { $stKnownIssueCounts[$ki] = 0 }; $stKnownIssueCounts[$ki]++                 } (})                 якщо ($mKEK) { $c.WithMissingKEK++; $cBatches["missing_kek"]. Add((Get-SlimDevice $device)) }                 якщо (-не $isUpd -and ($tskDis -або (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $c.TempFailures++; $cBatches["temp_failures"]. Add((Get-SlimDevice $device)) }                 якщо (-не $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -або ($tskNF -and $hasErr))) { $c.PermFailures++; $cBatches["perm_failures"]. Add((Get-SlimDevice $device)) }                 якщо ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $c.RolloutInProgress++; $cBatches["rollout_inprogress"]. Add((Get-SlimDevice $device)) }                 якщо ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -ne "InProgress") { $c.NotYetInitiated++ }                 якщо ($rStat -eq "InProgress" -and $e 1808 -eq 0) { $c.InProgress++ }                 # Очікування оновлення: застосовано політику або WinCS, стан очікується, SB ON, завдання не вимкнуто                 якщо ($isUpdatePending) {                     $c.Триває оновлення++; $cBatches["update_pending"]. Add((Get-SlimDevice $device))                 } (})                 якщо (-not $isUpd -and $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) }                 # У розділі Пристрої спостереження (окремо від обов'язкових дій)                 якщо (-не $isUpd -and (Test-ConfidenceLevel $conf 'UnderObservation')) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) }                 # Дія Обов'язковий параметр: не оновлено, SB ON, not matching other confidence categories, not Update Pending                 if (-not $isUpd -and $sbOn -and -not $isUpdatePending -and -not (Test-ConfidenceLevel $conf 'HighConfidence') -and -not (Test-ConfidenceLevel $conf 'UnderObservation') -and -not (Test-ConfidenceLevel $conf 'TemporarilyPaused') -and -not (Test-ConfidenceLevel $conf 'NotSupported')) {                     $cBatches["action_required"]. Add((Get-SlimDevice $device))                 } (})                 якщо (-не $stMfrCounts.ContainsKey($mfr)) { $stMfrCounts[$mfr] = @{ Total=0; Оновлено=0; Очікування оновлення=0; HighConf=0; UnderObs=0; ActionReq=0; TempPaused=0; NotSupported=0; SBOff=0; З помилками=0 } }                 $stMfrCounts[$mfr]. Усього++                 якщо ($isUpd) { $stMfrCounts[$mfr]. Оновлено++ }                 elseif (-not $sbOn) { $stMfrCounts[$mfr]. SBOff++ }                 elseif ($isUpdatePending) { $stMfrCounts[$mfr]. Триває оновлення++ }                 elseif (Test-ConfidenceLevel $conf "HighConfidence") { $stMfrCounts[$mfr]. HighConf++ }                 elseif (Test-ConfidenceLevel $conf "UnderObservation") { $stMfrCounts[$mfr]. UnderObs++ }                 elseif (Test-ConfidenceLevel $conf "TemporarilyPaused") { $stMfrCounts[$mfr]. TempPaused++ }                 elseif (Test-ConfidenceLevel $conf "NotSupported") { $stMfrCounts[$mfr]. Не підтримується++ }                 інакше { $stMfrCounts[$mfr]. ActionReq++ }                 якщо ($hasErr) { $stMfrCounts[$mfr]. З помилками++ }                 # Відстеження всіх пристроїв за блоком (включно з пустим BucketId)                 $bucketKey = якщо ($bid -та $bid -ne "") { $bid } інакше { "(пусто)" }                 якщо (-не $stAllBuckets.ContainsKey($bucketKey)) {                     $stAllBuckets[$bucketKey] = @{ Count=0; Оновлено=0; Виробник=$mfr; Model=""; BIOS="" }                     якщо ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. Model = $device. WMI_Model }                     якщо ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription }                 } (})                 $stAllBuckets[$bucketKey]. Кількість++                 якщо ($isUpd) { $stAllBuckets[$bucketKey]. Оновлено++ }             } (})             # Очищення пакетів на диск             foreach ($df in $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] }             $rawDevices = $null; $cBatches = $null; [System.GC]::Collect()             $cSw.Stop()             $cTime = [Математика]::Round($cSw.Elapsed.TotalSeconds, 1)             $cRem = $stTotalChunks - $ci - 1             $cEta = якщо ($cRem -gt 0) { " | ETA: ~$([Математика]::Round($cRem * $cSw.Elapsed.TotalSeconds / 60, 1)) min" } інакше { "" }             $cMem = [math]::Round([System.GC]::GetTotalMemory($false) / 1MB, 0)             якщо ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem }             Write-Host " +$cNew нові, $cDupe dupes, ${cTime}s | Мем: ${cMem}MB$cEta" -ForegroundColor Green         } (})         # Завершення створення масивів JSON         foreach ($dfName in $stDeviceFiles) {             [System.IO.File]::AppendAllText($stDeviceFilePaths[$dfName], "n]", [System.Text.Encoding]::UTF8)             Write-Host " $dfName.json: $($stDeviceFileCounts[$dfName]) пристрої" -ForegroundColor DarkGray         } (})         # Обчислення похідної статистики         $stAtRisk = 0; $stSafeList = 0         foreach ($bid in $stAllBuckets.Keys) {             $b = $stAllBuckets[$bid]; $nu = $b.Count – $b.Оновлено             якщо ($stFailedBuckets.Contains($bid)) { $stAtRisk += $nu }             elseif ($stSuccessBuckets.Contains($bid)) { $stSafeList += $nu }         } (})         $stAtRisk = [math]::Max(0; $stAtRisk – $c.WithErrors)         # NotUptodate = count from not_updated batch (devices with SB ON and not updated)         $stNotUptodate = $stDeviceFileCounts["not_updated"]         $stats = [ordered]@{             ReportGeneratedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss")             TotalDevices = $c.Total; SecureBootEnabled = $c.SBEnabled; SecureBootOFF = $c.SBOff             Оновлено = $c.Оновлено; HighConfidence = $c.HighConf; UnderObservation = $c.UnderObs             ActionRequired = $c.ActionReq; TemporarilyPaused = $c.TempPaused; NotSupported = $c.NotSupported             NoConfidenceData = $c.NoConfData; TaskDisabled = $c.TaskDisabled; TaskNotFound = $c.TaskNotFound             TaskDisabledNotUpdated = $c.TaskDisabledNotUpdated             CertificatesUpdated = $c.Updated; NotUptodate = $stNotUptodate; FullyUpdated = $c.Updated             Оновлення в очікуванні = $stNotUptodate; UpdatesComplete = $c.Updated             WithErrors = $c.WithErrors; InProgress = $c.InProgress; NotYetInitiated = $c.NotYetInitiated             RolloutInProgress = $c.RolloutInProgress; WithKnownIssues = $c.WithKnownIssues             WithMissingKEK = $c.WithMissingKEK; TemporaryFailures = $c.TempFailures; PermanentFailures = $c.PermFailures             NeedsReboot = $c.NeedsReboot; Триває оновлення = $c.Оновлення очікується             AtRiskDevices = $stAtRisk; SafeListDevices = $stSafeList             PercentWithErrors = if ($c.Total -gt 0) { [math]::Round(($c.WithErrors/$c.Total)*100,2) } інакше { 0 }             PercentAtRisk = if ($c.Total -gt 0) { [math]::Round(($stAtRisk/$c.Total)*100,2) } інакше { 0 }             PercentSafeList = якщо ($c.Total -gt 0) { [math]::Round(($stSafeList/$c.Total)*100,2) } ще { 0 }             PercentHighConfidence = if ($c.Total -gt 0) { [math]::Round(($c.HighConf/$c.Total)*100,1) } інакше { 0 }             PercentCertUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } інакше { 0 }             PercentActionRequired = if ($c.Total -gt 0) { [math]::Round(($c.ActionReq/$c.Total)*100,1) } інакше { 0 }             PercentNotUptodate = if ($c.Total -gt 0) { [math]::Round($stNotUptodate/$c.Total*100,1) } інакше { 0 }             PercentFullyUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } ще { 0 }             UniqueBuckets = $stAllBuckets.Count; PeakMemoryMB = $stPeakMemMB; ProcessingMode = "Streaming" (Потокове передавання)         } (})         # Записування CSVs         [PSCustomObject]$stats | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_Summary_$timestamp.csv") -NoTypeInformation -Encoding UTF8         $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } – за спаданням | ForEach-Object {             [PSCustomObject]@{ Виробник=$_. Ключових; Кількість=$_. Значення.Підсумок; Оновлено=$_. Значення.Оновлено; HighConfidence=$_. Value.HighConf; ActionRequired=$_. Value.ActionReq }         } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_ByManufacturer_$timestamp.csv") -NoTypeInformation -Encoding UTF8         $stErrorCodeCounts.GetEnumerator() | Sort-Object значення –за спаданням | ForEach-Object {             [PSCustomObject]@{ ErrorCode=$_. Ключових; Кількість=$_. Значення; SampleDevices=($stErrorCodeSamples[$_. Ключ] -join ", ") }         } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_ErrorCodes_$timestamp.csv") -NoTypeInformation -Encoding UTF8         $stAllBuckets.GetEnumerator() | Sort-Object { $_. Value.Count } – за спаданням | ForEach-Object {             [PSCustomObject]@{ BucketId=$_. Ключових; Кількість=$_. Value.Count; Оновлено=$_. Значення.Оновлено; NotUpdated=$_. Значення.Кількість-$_. Значення.Оновлено; Виробник =$_. Value.Manufacturer }         } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_UniqueBuckets_$timestamp.csv") -NoTypeInformation -Encoding UTF8         # Створення CSVs, сумісних з orchestrator (очікувані імена файлів для Start-SecureBootRolloutOrchestrator.ps1)         $notUpdatedJsonPath = Join-Path $dataDir "not_updated.json"         якщо (тестовий $notUpdatedJsonPath) {             спробуйте {                 $nuData = Get-Content $notUpdatedJsonPath -Raw | Перетворити файл із формату Json                 якщо ($nuData.Count -gt 0) {                     # NotUptodate CSV – orchestrator шукає *NotUptodate*.csv                     $nuData | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_NotUptodate_$timestamp.csv") -NoTypeInformation -Encoding UTF8                     Write-Host " Orchestrator CSV: SecureBoot_NotUptodate_$timestamp.csv (пристрої$($nuData.Count)" -ForegroundColor Gray                 } (})             } зловити { }         } (})         # Записування даних JSON для приладної дошки         $stats | ConvertTo-Json -Глибина 3 | Set-Content (join-path $dataDir "summary.json") -Encoding UTF8         # ІСТОРИЧНЕ ВІДСТЕЖЕННЯ: збереження точки даних для діаграми тренду         # Використовуйте стабільне розташування кеша, щоб дані тренду зберігалися в папках агрегації з позначками часу.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   # Якщо outputPath має вигляд "...\Aggregation_yyyyMMdd_HHmmss", кеш переходить до батьківської папки.# В іншому разі кеш передається в самій програмі OutputPath.$parentDir = Split-Path $OutputPath -Parent         $leafName = Split-Path $OutputPath -листок         якщо ($leafName -match "^Aggregation_\d{8}" -або $leafName -eq "Aggregation_Current") {             # Orchestrator-created timestamped folder – використовуйте батьківський елемент для стабільного кеша             $historyPath = Join-Path $parentDir ".cache\trend_history.json"         } інакше {             $historyPath = Join-Path $OutputPath ".cache\trend_history.json"         } (})         $historyDir = Split-Path $historyPath -Parent         if (-not (test-path $historyDir)) { New-Item -ItemType Directory -Path $historyDir -Force | Out-Null }         $historyData = @()         якщо ($historyPath тестового шляху) {             спробуйте { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } улов { $historyData = @() }         } (})         # Також перевірте в outputPath\.cache\ (застаріле розташування з попередніх версій)         # Об'єднання будь-яких точок даних, які ще не містяться в основному журналі         якщо ($leafName -eq "Aggregation_Current" -або $leafName -match "^Aggregation_\d{8}") {             $innerHistoryPath = Join-Path $OutputPath ".cache\trend_history.json"             if ((Test-Path $innerHistoryPath) -and $innerHistoryPath -ne $historyPath) {                 спробуйте {                     $innerData = @(Get-Content $innerHistoryPath -Raw | Перетворити на формат Json                     $existingDates = @($historyData | ForEach-Object { $_. Дата })                     foreach ($entry in $innerData) {                         якщо ($entry. Дата та $entry. Дата –notin $existingDates) {                             $historyData += $entry                         } (})                     } (})                     якщо ($innerData.Count -gt 0) {                         Write-Host " Merged $($innerData.Count) data points from inner cache" -ForegroundColor DarkGray                     } (})                 } зловити { }             } (})         }

        # BOOTSTRAP: Якщо історія тренду порожня/розріджена, реконструюйте з історичних даних         якщо ($historyData.Count -lt 2 -and ($leafName - match "^Aggregation_\d{8}" -або $leafName -eq "Aggregation_Current")) {             Write-Host " Bootstrapping журнал тренду з історичних даних..." -ForegroundColor Yellow             $dailyData = @{}                          # Джерело 1: Зведені CSVs всередині поточної папки (Aggregation_Current зберігає всі резюме CSVs)             $localSummaries = Get-ChildItem $OutputPath -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | ім'я Sort-Object             foreach ($summCsv in $localSummaries) {                 спробуйте {                     $summ = Import-Csv $summCsv.FullName | Select-Object –Перший 1                     якщо ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0 -and $summ. ReportGeneratedAt) {                         $dateStr = ([дата й час]$summ. ReportGeneratedAt). ToString("yyyy-MM-dd")                         $updated = якщо ($summ. Оновлено) { [int]$summ. Оновлено } ще { 0 }                         $notUpd = якщо ($summ. NotUptodate) { [int]$summ. NotUptodate } інакше { [int]$summ. TotalDevices – $updated }                         $dailyData[$dateStr] = [PSCustomObject]@{                             Date = $dateStr; Усього = [int]$summ. TotalDevices; Оновлено = $updated; NotUpdated = $notUpd                             NeedsReboot = 0; Помилки = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } інакше { 0 }                         } (})                     } (})                 } зловити { }             } (})                          # Джерело 2. Папки зі старою позначкою часу Aggregation_* (застарілі, якщо вони існують)             $aggFolders = Get-ChildItem $parentDir -Directory -Filter "Aggregation_*" -EA SilentlyContinue |                 Where-Object { $_. Ім'я -збігається з '^Aggregation_\d{8}' } |                 ім'я Sort-Object             foreach ($folder in $aggFolders) {                 $summCsv = Get-ChildItem $folder. FullName – фільтр "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Select-Object –Перший 1                 якщо ($summCsv) {                     спробуйте {                         $summ = Import-Csv $summCsv.FullName | Select-Object –Перший 1                         якщо ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0) {                             $dateStr = $folder. Ім'я -replace '^Aggregation_(\d{4})(\d{2})(\d{2})_.*', '$1-$2-$3'                             $updated = якщо ($summ. Оновлено) { [int]$summ. Оновлено } ще { 0 }                             $notUpd = якщо ($summ. NotUptodate) { [int]$summ. NotUptodate } інакше { [int]$summ. TotalDevices – $updated }                             $dailyData[$dateStr] = [PSCustomObject]@{                                 Date = $dateStr; Усього = [int]$summ. TotalDevices; Оновлено = $updated; NotUpdated = $notUpd                                 NeedsReboot = 0; Помилки = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } інакше { 0 }                             } (})                         } (})                     } зловити { }                 } (})             } (})                          # Джерело 3: RolloutState.json WaveHistory (має часові позначки на хвилі з першого дня)             # Це забезпечує точки даних базового плану, навіть якщо немає старих папок агрегації             $rolloutStatePaths = @(                 (Join-Path $parentDir "RolloutState\RolloutState.json"),                 (Join-Path $OutputPath "RolloutState\RolloutState.json")             )             foreach ($rsPath in $rolloutStatePaths) {                 якщо ($rsPath тестового шляху) {                     спробуйте {                         $rsData = Get-Content $rsPath -Raw | Перетворити файл із формату Json                         якщо ($rsData.WaveHistory) {                             # Використовувати дати початку хвилі як точки даних тренду                             # Обчислення сукупних пристроїв, призначених для кожної хвилі                             $cumulativeTargeted = 0                             foreach ($wave in $rsData.WaveHistory) {                                 якщо ($wave. StartedAt – і $wave. Кількість пристроїв) {                                     $waveDate = ([дата й час]$wave. StartedAt). ToString("yyyy-MM-dd")                                     $cumulativeTargeted += [int]$wave. Кількість пристроїв                                     якщо (-не $dailyData.ContainsKey($waveDate)) {                                         # Приблизний: під час початку хвилі оновлено лише пристрої з попередніх хвиль                                         $dailyData[$waveDate] = [PSCustomObject]@{                                             Date = $waveDate; Total = $c.Total; Оновлено = [math]::Max(0; $cumulativeTargeted - [int]$wave. DeviceCount)                                             NotUpdated = $c.Total – [math]::Max(0; $cumulativeTargeted - [int]$wave. DeviceCount)                                             NeedsReboot = 0; Помилки = 0; ActionRequired = 0                                         } (})                                     } (})                                 } (})                             } (})                         } (})                     } зловити { }                     break # Use first found                 } (})             }

            якщо ($dailyData.Count -gt 0) {                 $historyData = @($dailyData.GetEnumerator() | ключ Sort-Object | ForEach-Object { $_. Значення })                 Write-Host " Bootstrapped $($historyData.Count) data points from historical summaries" -ForegroundColor Green             } (})         }

        # Додати поточну точку даних (дедуплікація за днями – зберегти останні дані на день)         $todayKey = (Get-Date). ToString("yyyy-MM-dd")         $existingToday = $historyData | Where-Object { "$($_. Дата)" -like "$todayKey*" }         якщо ($existingToday) {             # Замінити сьогоднішню заявку             $historyData = @($historyData | Where-Object { "$($_. Date)" -notlike "$todayKey*" })         } (})         $historyData += [PSCustomObject]@{             Date = $todayKey             Усього = $c.Total             Оновлено = $c.Оновлено             NotUpdated = $stNotUptodate             NeedsReboot = $c.NeedsReboot             Помилки = $c.З помилками             ActionRequired = $c.ActionReq         } (})         # Видаліть погані точки даних (0 разом) і зберігайте останні 90         $historyData = @($historyData | Where-Object { [int]$_. Усього -gt 0 })         # Без обмеження – дані тренду ~ 100 байт/запис, повний рік = ~36 КБ         $historyData | ConvertTo-Json -Глибина 3 | Set-Content $historyPath -кодування UTF8         Write-Host " Журнал тренду: $($historyData.Count) точки даних" -ForegroundColor DarkGray                  # Створення даних діаграми тренду для HTML         $trendLabels = ($historyData | ForEach-Object { "'$($_. Date)'" }) -join ","         $trendUpdated = ($historyData | ForEach-Object { $_. Оновлено }) -join ","         $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) -join ","         $trendTotal = ($historyData | ForEach-Object { $_. Усього }) -join ","         # Проекція: розширення лінії тренду за допомогою експоненційного подвоєння (2,4,8,16...)         # Отримує розмір хвилі та період спостереження від фактичних даних журналу тренду.# - Розмір хвилі = найбільше одномісячне збільшення, що спостерігається в історії (остання хвиля розгорнута)         # - Дні спостереження = середні календарні дні між точками даних тренду (частота запуску)         # Потім подвоює розмір хвилі кожного періоду, зіставляючи стратегію зростання orchestrator 2x.$projLabels = ""; $projUpdated = ""; $projNotUpdated = ""; $hasProjection = $false         якщо ($historyData.Count -ge 2) {             $lastUpdated = $c.Оновлено             $remaining = $stNotUptodate # Лише неоновлювалося SB-ON пристроїв (виключає SecureBoot OFF)             $projDates = @(); $projValues = @(); $projNotUpdValues = @()             $projDate = Get-Date

            # Отримання розміру хвилі та періоду спостереження з журналу тренду             $increments = @()             $dayGaps = @()             для ($hi = 1; $hi -lt $historyData.Count; $hi++) {                 $inc = $historyData[$hi]. Оновлено – $historyData[$hi-1]. Оновлено                 якщо ($inc -gt 0) { $increments += $inc }                 спробуйте {                     $d 1 = [дата й час]::P arse($historyData[$hi-1]. Дата)                     $d 2 = [дата й час]::P arse($historyData[$hi]. Дата)                     $gap = ($d 2 - $d 1). Усього днів                     якщо ($gap -gt 0) { $dayGaps += $gap }                 } зловити {}             } (})             # Розмір хвилі = останній додатний приріст (поточна хвиля), резервний до середнього, мінімум 2             $waveSize = якщо ($increments. Кількість -gt 0) {                 [math]::Max(2; $increments[-1])             } ще { 2 }             # Період спостереження = середній проміжок між точками даних (календарні дні на хвилі), мінімум 1             $waveDays = якщо ($dayGaps.Count -gt 0) {                 [math]::Max(1; [math]::Round(($dayGaps | Measure-Object -Average). Середнє, 0))             } ще { 1 }

            Write-Host " Проекція: waveSize=$waveSize (від останнього кроку), waveDays=$waveDays (avg gap from history)" -ForegroundColor DarkGray

            $dayCounter = 0             # Project до оновлення всіх пристроїв або максимальної кількості 365 днів             для ($pi = 1; $pi -le 365; $pi++) {                 $projDate = $projDate.AddDays(1)                 $dayCounter++                 # На кожній межі періоду спостереження розгорніть хвилю, а потім двічі                 якщо ($dayCounter -ge $waveDays) {                     $devicesThisWave = [math]::Min($waveSize; $remaining)                     $lastUpdated += $devicesThisWave                     $remaining -= $devicesThisWave                     якщо ($lastUpdated -gt ($c.Оновлено + $stNotUptodate)) { $lastUpdated = $c.Оновлено + $stNotUptodate; $remaining = 0 }                     # Розмір подвійної хвилі для наступного періоду (стратегія orchestrator 2x)                     $waveSize = $waveSize * 2                     $dayCounter = 0                 } (})                 $projDates += "'$($projDate.ToString("yyyy-MM-dd")")"                 $projValues += $lastUpdated                 $projNotUpdValues += [math]::Max(0; $remaining)                 якщо ($remaining -le 0) { break }             } (})             $projLabels = $projDates -join ","             $projUpdated = $projValues -join ","             $projNotUpdated = $projNotUpdValues -join ","             $hasProjection = $projDates.Count -gt 0         } elseif ($historyData.Count -eq 1) {             Write-Host "Проекція: потрібно принаймні 2 точки даних тренду, щоб отримати хронометраж хвилі" -ForegroundColor DarkGray         } (})         # Створення комбінованих рядків даних діаграми для цього рядка         $allChartLabels = якщо ($hasProjection) { "$trendLabels,$projLabels" } інакше { $trendLabels }         $projDataJS = якщо ($hasProjection) { $projUpdated } інакше { "" }         $projNotUpdJS = якщо ($hasProjection) { $projNotUpdated } інакше { "" }         $histCount = ($historyData | Мірний об'єкт). Розраховувати         $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } – за спаданням | ForEach-Object {             @{ name=$_. Ключових; total=$_. Значення.Підсумок; оновлено=$_. Значення.Оновлено; highConf=$_. Value.HighConf; actionReq=$_. Value.ActionReq }         } | ConvertTo-Json -Глибина 3 | Set-Content ($dataDir "manufacturers.json") -Кодування UTF8         # Перетворення файлів даних JSON на CSV для доступних людиною завантажень Excel         Write-Host "Перетворення даних пристрою на CSV для завантаження Excel..." -ForegroundColor Gray         foreach ($dfName in $stDeviceFiles) {             $jsonFile = Join-Path $dataDir "$dfName.json"             $csvFile = Join-Path $OutputPath "SecureBoot_${dfName}_$timestamp.csv"             якщо ($jsonFile "Тестовий шлях") {                 спробуйте {                     $jsonData = Get-Content $jsonFile -Raw | Перетворити файл із формату Json                     якщо ($jsonData.Count -gt 0) {                         # Включити додаткові стовпці для update_pending CSV                         $selectProps = якщо ($dfName -eq "update_pending") {                             @("Ім'я хоста", "WMI_Manufacturer", "WMI_Model", "BucketId", "ConfidenceLevel", "IsUpdated", "UEFICA2023Status", "UEFICA2023Error", "AvailableUpdatesPolicy", "WinCSKeyApplied", "SecureBootTaskStatus")                         } інакше {                             @("Ім'я хоста", "WMI_Manufacturer", "WMI_Model", "BucketId", "ConfidenceLevel", "IsUpdated", "UEFICA2023Error", "SecureBootTaskStatus", "KnownIssueId", "SkipReasonKnownIssue")                         } (})                         $jsonData | Select-Object $selectProps |                             Export-Csv -Path $csvFile -NoTypeInformation -Encoding UTF8                         Write-Host " $dfName -> рядків $($jsonData.Count) -> CSV" -ForegroundColor DarkGray                     } (})                 } зловити { Write-Host " $dfName - пропущено" -ForegroundColor DarkYellow }             } (})         } (})         # Створення самостійної html-приладної дошки         $htmlPath = Join-Path $OutputPath "SecureBoot_Dashboard_$timestamp.html"         Write-Host "Створення самостійної html-приладної дошки..." -ForegroundColor Yellow         # VELOCITY PROJECTION: Calculate from scan history or previous summary         $stDeadline = [datetime]"2026-06-24" # KEK cert expiry         $stDaysToDeadline = [math]::Max(0; ($stDeadline - (Get-Date)). Дні)         $stDevicesPerDay = 0         $stProjectedDate = $null         $stVelocitySource = "Н/Д"         $stWorkingDays = 0         $stCalendarDays = 0         # Спробуйте спочатку журнал тренду (легкий, уже підтримується агрегатором – замінює роздуте ScanHistory.json)         якщо ($historyData.Count -ge 2) {             $validHistory = @($historyData | Where-Object { [int]$_. Усього -gt 0 -і [int]$_. Оновлено -ge 0 })             якщо ($validHistory.Count -ge 2) {                 $prev = $validHistory[-2]; $curr = $validHistory[-1]                 $prevDate = [datetime]::P arse($prev. Date.Substring(0; [Math]::Min(10; $prev. Date.Length)))                 $currDate = [дата й час]::P arse($curr. Date.Substring(0; [Math]::Min(10; $curr. Date.Length)))                 $daysDiff = ($currDate - $prevDate). Усього днів                 якщо ($daysDiff -gt 0) {                     $updDiff = [int]$curr. Оновлено – [int]$prev. Оновлено                     якщо ($updDiff -gt 0) {                         $stDevicesPerDay = [math]::Round($updDiff / $daysDiff, 0)                         $stVelocitySource = "TrendHistory"                     } (})                 } (})             } (})         } (})         # Спробуйте зведення розгортання orchestrator (має попередньо обчислювану швидкість)         якщо ($stVelocitySource -eq "N/A" -and $RolloutSummaryPath -and (Test-Path $RolloutSummaryPath)) {             спробуйте {                 $rolloutSummary = Get-Content $RolloutSummaryPath -Raw | Перетворити файл із формату Json                 якщо ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) {                     $stDevicesPerDay = [math]::Round([double]$rolloutSummary.DevicesPerDay, 1)                     $stVelocitySource = "Orchestrator"                     якщо ($rolloutSummary.ProjectedCompletionDate) {                         $stProjectedDate = $rolloutSummary.ProjectedCompletionDate                     } (})                     якщо ($rolloutSummary.WorkingDaysRemaining) { $stWorkingDays = [int]$rolloutSummary.WorkingDaysRemaining }                     if ($rolloutSummary.CalendarDaysRemaining) { $stCalendarDays = [int]$rolloutSummary.CalendarDaysRemaining }                 } (})             } зловити { }         } (})         # Резервний варіант: спробуйте попередній зведений CSV-файл (пошук у поточній папці AND батьківських і споріднених папок агрегації)         якщо ($stVelocitySource -eq "N/A") {             $searchPaths = @(                 (Об'єднання $OutputPath "SecureBoot_Summary_*.csv")             )             # Також пошук споріднених папок агрегації (orchestrator створює нову папку кожен запуск)             $parentPath = Split-Path $OutputPath -Parent             якщо ($parentPath) {                 $searchPaths += ($parentPath "Aggregation_*\SecureBoot_Summary_*.csv")                 $searchPaths += ($parentPath "SecureBoot_Summary_*.csv")             } (})             $prevSummary = $searchPaths | ForEach-Object { Get-ChildItem $_ -EA SilentlyContinue } | Sort-Object LastWriteTime – за спаданням | Select-Object –Перший 1             якщо ($prevSummary) {                 спробуйте {                     $prevStats = Get-Content $prevSummary.FullName | Перетворення файлу CSV                     $prevDate = [datetime]$prevStats.ReportGeneratedAt                     $daysSinceLast = ((Get-Date) - $prevDate). Усього днів                     якщо ($daysSinceLast -gt 0,01) {                         $prevUpdated = [int]$prevStats.Updated                         $updDelta = $c.Updated – $prevUpdated                         якщо ($updDelta -gt 0) {                             $stDevicesPerDay = [математика]::Round($updDelta / $daysSinceLast; 0)                             $stVelocitySource = "Попередній звіт"                         } (})                     } (})                 } зловити { }             } (})         } (})         # Резервний варіант: обчислити швидкість із повного інтервалу журналу тренду (перший і останній пункт даних)         якщо ($stVelocitySource -eq "N/A" -and $historyData.Count -ge 2) {             $validHistory = @($historyData | Where-Object { [int]$_. Усього -gt 0 -і [int]$_. Оновлено -ge 0 })             якщо ($validHistory.Count -ge 2) {                 $first = $validHistory[0]                 $last = $validHistory[-1]                 $firstDate = [дата й час]::P arse($first. Date.Substring(0; [Math]::Min(10; $first. Date.Length)))                 $lastDate = [datetime]::P arse($last. Date.Substring(0; [Math]::Min(10; $last. Date.Length)))                 $daysDiff = ($lastDate - $firstDate). Усього днів                 якщо ($daysDiff -gt 0) {                     $updDiff = [int]$last. Оновлено – [int]$first. Оновлено                     якщо ($updDiff -gt 0) {                         $stDevicesPerDay = [math]::Round($updDiff / $daysDiff, 1)                         $stVelocitySource = "TrendHistory"                     } (})                 } (})             } (})         } (})         # Обчислення проекції за допомогою експоненційного подвоєння (відповідно до діаграми тренду)         # Повторне використання даних проекції, уже обчислених для діаграми, якщо вони доступні         якщо ($hasProjection -and $projDates.Count -gt 0) {             # Використовувати останню прогнозовану дату (коли всі пристрої оновлюються)             $lastProjDateStr = $projDates[-1] -replace "'", ""             $stProjectedDate = ([дата й час]::P arse($lastProjDateStr)). ToString("MMM dd, yyyy")             $stCalendarDays = ([дата й час]::P arse($lastProjDateStr) - (Get-Date)). Днів             $stWorkingDays = 0             $d = Get-Date             для ($i = 0; $i -lt $stCalendarDays; $i++) {                 $d = $d.AddDays(1)                 якщо ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ }             } (})         } elseif ($stDevicesPerDay -gt 0 -and $stNotUptodate -gt 0) {             # Резервний варіант: лінійна проекція, якщо немає експоненційних даних             $daysNeeded = [math]::Ceiling($stNotUptodate / $stDevicesPerDay)             $stProjectedDate = (Get-Date). AddDays($daysNeeded). ToString("MMM dd, yyyy")             $stWorkingDays = 0; $stCalendarDays = $daysNeeded             $d = Get-Date             для ($i = 0; $i -lt $daysNeeded; $i++) {                 $d = $d.AddDays(1)                 якщо ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ }             } (})         } (})         # Побудувати швидкість HTML         $velocityHtml = якщо ($stDevicesPerDay -gt 0) {             "<div><strong>&#128640; Пристрої/день:</strong> $($stDevicesPerDay.ToString('N0')) (джерело: $stVelocitySource)</div>" +             "<div><сильний>&#128197; Прогнозоване завершення:</strong> $stProjectedDate" +             $(якщо ($stProjectedDate -and [datetime]::P arse($stProjectedDate) -gt; $stDeadline) { " <span style='color:#dc3545; font-weight:bold'>&#9888; PAST DEADLINE</span>" } інакше { " <span style='color:#28a745'>&#10003; До крайнього терміну</span>" }) +             "</div>" +             "<div><strong>&#128336; Робочі дні:</сильний> $stWorkingDays | <сильний>Calendar Днів:</strong> $stCalendarDays</div>" +             "<div style='font-size:.8em; color:#888'>Крайній термін: 24 червня 2026 (термін дії сертифіката KEK завершується) | Залишилося днів: $stDaysToDeadline</div>"         } інакше {             "<div style='padding:8px; тло:#fff3cd; радіус межі:4px; межа зліва:3px суцільна #ffc107'>" +             "<сильні>&#128197; Прогнозоване завершення:</strong> Недостатньо даних для обчислення швидкості.                                                                                  " + "+"             "Запуск агрегації принаймні двічі зі змінами даних для встановлення rate.<br/>" +             "<сильний>Крайній термін:</strong> 24 червня 2026 року (термін дії сертифіката KEK завершується) | <залишилося сильних>днів:</strong> $stDaysToDeadline</div>"         } (})                  # Зворотний відлік терміну дії сертифіката         $certToday = Get-Date         $certKekExpiry = [дата й час]"2026-06-24"         $certUefiExpiry = [дата й час]"2026-06-27"         $certPcaExpiry = [дата й час]"2026-10-19"         $daysToKek = [math]::Max(0; ($certKekExpiry - $certToday). Дні)         $daysToUefi = [math]::Max(0; ($certUefiExpiry - $certToday). Дні)         $daysToPca = [math]::Max(0; ($certPcaExpiry - $certToday). Дні)         $certUrgency = якщо ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } інакше { '#28a745' }                  #Helper: Read records from JSON, build bucket summary + first N device rows         $maxInlineRows = 200         функція Build-InlineTable {             param([string]$JsonPath; [int]$MaxRows = 200; [рядок]$CsvFileName = "")             $bucketSummary = ""             $deviceRows = ""             $totalCount = 0             якщо ($JsonPath тестового шляху) {                 спробуйте {                     $data = Get-Content $JsonPath -Raw | Перетворити файл із формату Json                     $totalCount = $data. Розраховувати                                          #BUCKET SUMMARY: Group by BucketId, show counts per bucket with Updated from global bucket stats                     якщо ($totalCount -gt 0) {                         $buckets = $data | Group-Object BucketId | кількість Sort-Object за спаданням                         $bucketSummary = "><2 h3 style='font-size:.95em; колір:#333; margin:10px 0 5px'><3 By Hardware Bucket ($($buckets. Кількість) блоків)><4 /h3>"                         $bucketSummary += "><6 div style='max-height:300px; переповнення-y:auto; margin-bottom:15px'><table><thead><tr><><5 BucketID><6 /th><th style='text-align:right'>Total</th><th style='text-align:right; color:#28a745'>Оновлено</th><th style='text-align:right; color:#dc3545'>Not Updated</th><th><1 Виробник><2 /th></tr></thead><tbody>"                         foreach ($b in $buckets) {                             $bid = якщо ($b.Name) { $b.Name } інакше { "(пусто)" }                             $mfr = ($b.Group | Select-Object -Перший 1). WMI_Manufacturer                             # Отримати оновлену кількість від глобальної статистики сегмента (усі пристрої в цьому блоці по всьому набору даних)                             $lookupKey = $bid                             $globalBucket = якщо ($stAllBuckets.ContainsKey($lookupKey)) { $stAllBuckets[$lookupKey] } інакше { $null }                             $bUpdatedGlobal = якщо ($globalBucket) { $globalBucket.Оновлено } ще { 0 }                             $bTotalGlobal = якщо ($globalBucket) { $globalBucket.Count } ще { $b.Count }                             $bNotUpdatedGlobal = $bTotalGlobal - $bUpdatedGlobal                             $bucketSummary += "<tr><td style='font-size:.8em'>$bid><4 /td><td style='text-align:right; font-weight:bold'>$bTotalGlobal><8 /td><td style='text-align:right; color:#28a745; font-weight:bold'>$bUpdatedGlobal><2 /td><td style='text-align:right; color:#dc3545; font-weight:bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n"                         } (})                         $bucketSummary += "</tbody></table></div>"                     } (})                                          # DEVICE DETAIL: First N rows as flat list                     $slice = $data | Select-Object –перший $MaxRows                     foreach ($d in $slice) {                         $conf = $d.ConfidenceLevel                         $confBadge = якщо ($conf -match "High") { '<span class="badge-success">High Conf><2 /span>" }                                      elseif ($conf -match "Not Sup") { "<span class="badge-danger">Not Supported><6 /span>" }                                      elseif ($conf -match "Under") { "<span class="badge-info">Under Obs><0 /span>" }                                      elseif ($conf -match "Призупинено") { "<span class="badge-warning">Призупинено><4 /span>" }                                      інакше { '<span class="badge-warning">Action Req><8 /span>' }                         $statusBadge = якщо ($d.IsUpdated) { "><00 span class="badge-success"><01 Оновлено</span>" }                                        elseif ($d.UEFICA2023Error) { '><04 span class="badge-danger"><05 Помилка</span>' }                                        інакше { '><08 span class="badge-warning"><09 Очікується><0 /span>' }                         $deviceRows += "><12 tr><td><5 $($d.HostName)><16 /td><td><9 $($d.WMI_Manufacturer)><20><9 /td><td><3 $($d.WMI_Model)><24 /td><td><7 $confBadge><8 /td><td><1 $statusBadge><2 //td><td><5 $(if($d.UEFICA2023Error){$d.UEFICA2023Error}else{'-'})><36 $d /td><td style='font-size:.75em'><39 $($d.BucketId)><40 /td></tr><3 'n"                     } (})                 } зловити { }             } (})             якщо ($totalCount -eq 0) {                 return "><44 div style='padding:20px; color:#888; font-style:italic'><45 Немає пристроїв у цій категорії.><46 /div>"             } (})             $showing = [math]::Min($MaxRows; $totalCount)             $header = "><48 div style='margin:5px 0; font-size:.85em; color:#666'><49 Усього: $($totalCount.ToString("N0")) пристрої"             якщо ($CsvFileName) { $header += " | ><50 a href='$CsvFileName' style='color:#1a237e; font-weight:bold'>&#128196; Завантажити повний CSV-файл для Excel><3 /a>" }             $header += "><55 /div>"             $deviceHeader = "><57 h3 style='font-size:.95em; колір:#333; margin:10px 0 5px'><58 Device Details (відображається перша $showing)><59 /h3>"             $deviceTable = "><61 div style='max-height:500px; overflow-y:auto'><table><thead><tr><th><0 HostName><1 /th><><4 Manufacturer><5 /th><th><8 Model><9 /th><><2 Confidence><3 /th><><7 стану><6 /th><><0 Помилка><1 /th><><4 BucketId><5 /th></tr></thead><tbody><2 $deviceRows><3 /tbody></table></div>"             return "$header$bucketSummary$deviceHeader$deviceTable"         } (})                  # Створення вбудованих таблиць із файлів JSON, які вже є на диску, зв'язування з CSVs         $tblErrors = Build-InlineTable -JsonPath (join-path $dataDir "errors.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_errors_$timestamp.csv"         $tblKI = Build-InlineTable -JsonPath (Join-Path $dataDir "known_issues.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_known_issues_$timestamp.csv"         $tblKEK = Build-InlineTable -JsonPath (Join-Path $dataDir "missing_kek.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_missing_kek_$timestamp.csv"         $tblNotUpd = Build-InlineTable -JsonPath (Join-Path $dataDir "not_updated.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_not_updated_$timestamp.csv"         $tblTaskDis = Build-InlineTable -JsonPath (Join-Path $dataDir "task_disabled.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_task_disabled_$timestamp.csv"         $tblTemp = Build-InlineTable -JsonPath (Join-Path $dataDir "temp_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_temp_failures_$timestamp.csv"         $tblPerm = Build-InlineTable -JsonPath (Join-Path $dataDir "perm_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_perm_failures_$timestamp.csv"         $tblUpdated = Build-InlineTable -JsonPath (Join-Path $dataDir "updated_devices.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_updated_devices_$timestamp.csv"         $tblActionReq = Build-InlineTable -JsonPath (Join-Path $dataDir "action_required.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_action_required_$timestamp.csv"         $tblUnderObs = Build-InlineTable -JsonPath (Join-Path $dataDir "under_observation.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_under_observation_$timestamp.csv"         $tblNeedsReboot = Build-InlineTable -JsonPath (Join-Path $dataDir "needs_reboot.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_needs_reboot_$timestamp.csv"         $tblSBOff = Build-InlineTable -JsonPath (join-path $dataDir "secureboot_off.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_secureboot_off_$timestamp.csv"         $tblRolloutIP = Build-InlineTable -JsonPath (Join-Path $dataDir "rollout_inprogress.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_rollout_inprogress_$timestamp.csv"         # Настроювана таблиця для стовпців Оновлення в очікуванні; включає стовпці UEFICA2023Status і UEFICA2023Error         $tblUpdatePending = ""         $upJsonPath = Join-Path $dataDir "update_pending.json"         якщо ($upJsonPath тестового шляху) {             спробуйте {                 $upData = Get-Content $upJsonPath -Raw | Перетворити файл із формату Json                 $upCount = $upData.Count                 якщо ($upCount -gt 0) {                     $upHeader = "<div style='margin:5px 0; font-size:.85em; color:#666'>Усього: $($upCount.ToString("N0")) пристрої | <a href='SecureBoot_update_pending_$timestamp.csv' style='color:#1a237e; font-weight:bold'>&#128196; Завантажити повний CSV-файл для Excel><4 /a></div>"                     $upRows = ""                     $upSlice = $upData | Select-Object –перший $maxInlineRows                     foreach ($d in $upSlice) {                         $uefiSt = якщо ($d.UEFICA2023Status) { $d.UEFICA2023Status } else { '<span style="color:#999">null><0 /span>" }                         $uefiErr = якщо ($d.UEFICA2023Error) { "<span style='color:#dc3545'>$($d.UEFICA2023Error)</span>" } інакше { '-' }                         $policyVal = якщо ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } інакше { '-' }                         $wincsVal = якщо ($d.WinCSKeyApplied) { '<span class="badge-success">Так><8 /span>' } інакше { '-' }                         $upRows += "<tr><td><3 $($d.HostName)</td><td><7 $($d.WMI_Manufacturer)</td><td><1 $($d.WMI_Model)</td><td><5 $uefiSt><6 /td><td><9 $uefiErr><50 /td><td><53 $policyVal><54 /td><td><57 $wincsVal><58 /td><td style='font-size:.75em'>$($d.BucketId)</td></tr><65 'n"                     } (})                     $upShowing = [math]::Min($maxInlineRows; $upCount)                     $upDevHeader = "<h3 style='font-size:.95em; колір:#333; margin:10px 0 5px'>Device Details (відображається перша $upShowing)</h3>"                     $upTable = "<div style='max-height:500px; overflow-y:auto'><table><thead><tr><><9 HostName><0 /th><><3 Manufacturer><4 /1 модель><><7><8 /th><><1 UEFICA2023Status><2 /th><><5 UEFICA2 023Помилка><6 /th><><9 політика</th><>Ключ WinCS</th><><<BucketId /th></tr></thead><tbody><5 $upRows><6 /tbody></table></div>"                     $tblUpdatePending = "$upHeader$upDevHeader$upTable"                 } інакше {                     $tblUpdatePending = "<div style='padding:20px; color:#888; font-style:italic'>Немає пристроїв у цій категорії.</div>"                 } (})             } зловити {                 $tblUpdatePending = "<div style='padding:20px; color:#888; font-style:italic'>Немає пристроїв у цій категорії.</div>"             } (})         } інакше {             $tblUpdatePending = "<div style='padding:20px; color:#888; font-style:italic'>Немає пристроїв у цій категорії.</div>"         } (})                  # Зворотний відлік терміну дії сертифіката         $certToday = Get-Date         $certKekExpiry = [дата й час]"2026-06-24"         $certUefiExpiry = [дата й час]"2026-06-27"         $certPcaExpiry = [дата й час]"2026-10-19"         $daysToKek = [math]::Max(0; ($certKekExpiry - $certToday). Дні)         $daysToUefi = [math]::Max(0; ($certUefiExpiry - $certToday). Дні)         $daysToPca = [math]::Max(0; ($certPcaExpiry - $certToday). Дні)         $certUrgency = якщо ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } інакше { '#28a745' }                  # Вбудовані дані діаграми виробника (перші 10 за кількістю пристроїв)         $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } – за спаданням | Select-Object –Перші 10         $mfrChartTitle = якщо ($stMfrCounts.Count -le 10) { "За виробником" } ще { "Топ-10 виробників" }         $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_. Key)'" }) -join ","         $mfrUpdated = ($mfrSorted | ForEach-Object { $_. Value.Updated }) -join ","         $mfrUpdatePending = ($mfrSorted | ForEach-Object { $_. Value.UpdatePending }) -join ","         $mfrHighConf = ($mfrSorted | ForEach-Object { $_. Value.HighConf }) -join ","         $mfrUnderObs = ($mfrSorted | ForEach-Object { $_. Value.UnderObs }) -join ","         $mfrActionReq = ($mfrSorted | ForEach-Object { $_. Value.ActionReq }) -join ","         $mfrTempPaused = ($mfrSorted | ForEach-Object { $_. Value.TempPaused }) -join ","         $mfrNotSupported = ($mfrSorted | ForEach-Object { $_. Value.NotSupported }) -join ","         $mfrSBOff = ($mfrSorted | ForEach-Object { $_. Value.SBOff }) -join ","         $mfrWithErrors = ($mfrSorted | ForEach-Object { $_. Value.WithErrors }) -join ","                  # Таблиця виробника збірки         $mfrTableRows = ""         $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } – за спаданням | ForEach-Object {             $mfrTableRows += "<tr><td><7 $($_. Ключ)</td><td>$($_. Value.Total.ToString("N0")</td><td>$($_. Value.Updated.ToString("N0")</td><td>$($_. Value.HighConf.ToString("N0")><0 /td><td>$($_. Value.ActionReq.ToString("N0")><4 /td></tr>'n"         } (})                  # Фрагменти зблизька тегів HTML: розбиті на частини, щоб веб-платформи CMS не         # інтерпретувати їх як справжні HTML і впорснути невидимі символи Юнікод навколо них.$endScript = "</scr" + "ipt>"         $endStyle = "</sty" + "le>"         $endHead = "</he" + "ad>"         $endBody = "</bo" + "dy>"         $endHtml = "</ht" + "мл>"                  $htmlContent = @" <! HTML-> DOCTYPE <html lang="en"> <> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> заголовок <>приладної дошки стану сертифіката безпечного завантаження</title> <script src="https://cdn.jsdelivr.net/npm/chart.js">$endScript <стилі>< *{box-sizing:border-box; поле: 0; заповнення:0} body{font-family:'Segoe UI',Tahoma,sans-serif; тло:#f0f2f5; колір:#333} .header{background:linear-gradient(135deg;#1a237e,#0d47a1); color:#fff; заповнення:20px 30px} .header h1{font-size:1.6em; margin-bottom:5px} .header .meta{font-size:.85em; непрозорість:.9} .container{max-width:1400px; поле:0 авто; заповнення:20px} .cards{display:grid; grid-template-columns:repeat(auto-fill;minmax(170px,1fr)); проміжок: 12 пікселів; margin:20px 0} .card{background:#fff; радіус межі:10px; заповнення:15px; box-shadow:0 2px 8px rgba(0,0,0,.08); border-left:4px solid #ccc;transition:transform .2s} .card:hover{transform:translateY(-2px); box-shadow:0 4px 15px rgba(0,0,0,.12)} .card .value{font-size:1.8em; font-weight:700} .card .label{font-size:.8em; color:#666; margin-top:4px} .card .pct{font-size:.75em; колір:#888} .section{background:#fff; радіус межі:10px; заповнення:20px; margin:15px 0; box-shadow:0 2px 8px rgba(0,0,0,.08)} .section h2{font-size:1.2em; color:#1a237e; margin-bottom:10px; курсор:вказівник; user-select:none} .section h2:hover{text-decoration:underline} .section-body{display:none} .section-body.open{display:block} .charts{display:grid; grid-template-columns:1fr 1fr; gap:20px; margin:20px 0} .chart-box{background:#fff; радіус межі:10px; заповнення:20px; box-shadow:0 2px 8px rgba(0,0,0,.08)} таблиця{width:100%; облямівка-згортання:згортання; font-size:.85em} th{background:#e8eaf6; заповнення:8px 10px; вирівнювання тексту:зліва; позиція:залипання; перші:0; z-індекс:1} td{padding:6px 10px; межа знизу:1px суцільна #eee} tr:hover{background:#f5f5f5} .badge{display:inline-block; padding:2px 8px;border-radius:10px; font-size:.75em; font-weight:700} .badge-success{background:#d4edda; колір:#155724} .badge-danger{background:#f8d7da; колір:#721c24} .badge-warning{background:#fff3cd; колір:#856404} .badge-info{background:#d1ecf1; колір:#0c5460} .top-link{float:right; font-size:.8em; color:#1a237e; text-decoration:none} .footer{text-align:center; заповнення:20px; color:#999; font-size:.8em} a{color:#1a237e}$endStyle $endHead <> основного тексту <div class="header">     <h1>приладну дошку стану сертифіката безпечного завантаження</h1>     <div class="meta">Створено: $($stats. ReportGeneratedAt) | Усього пристроїв: $($c.Total.ToString("N0")) | Унікальні блоки: $($stAllBuckets.Count)</div> </div> <div class="container">

<!-- картки KPI – доступні для клацання, зв'язані з розділами –-> <div class="cards">     <class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#dc3545; text-decoration:none; position:relative"><div style="position:absolute; top:8px; right:8px; тло:#dc3545; color:#fff; заповнення:1px 6px; радіус межі:8px; font-size:.65em; font-weight:700">PRIMARY</div><div class="value" style="color:#dc3545">$($stNotUptodate.ToString(""N0"))</div><div class="label">NOT UPDATED><6 /div><div class="pct">$($stats. PercentNotUptodate)% – ПОТРІБНА ДІЯ><0 /div></a><3     <class="card" href="#s-upd" onclick="openSection('d-upd')" style="border-left-color:#28a745; text-decoration:none; position:relative"><div style="position:absolute; top:8px; right:8px; тло:#28a745; color:#fff; заповнення:1px 6px; радіус межі:8px; font-size:.65em; font-weight:700">PRIMARY><8 /div><div class="value" style="color:#28a745">$($c.Updated.ToString(""N0"))</div><div class="label">Оновлено><6 /div><div class="pct">$($stats. PercentCertUpdated)%</div></a><3     <class="card" href="#s-sboff" onclick="openSection('d-sboff')" style="border-left-color:#6c757d; text-decoration:none; position:relative"><div style="position:absolute; top:8px; right:8px; тло:#6c757d; color:#fff; заповнення:1px 6px; радіус межі:8px; font-size:.65em; font-weight:700">PRIMARY><8 /div><div class="value"><1 $($c.SBOff.ToString("N0")><2 /div><div class="label"><5 SecureBoot OFF</div><div class="pct"><9 $(if($c.Total -gt 0){[math]::Round(($c.SBOff/$c.Total)*100,1)}інакше{0})% – поза областю><0 /div></a><3     <class="card" href="#s-nrb" onclick="openSection('d-nrb')" style="border-left-color:#ffc107; text-decoration:none"><div class="value" style="color:#ffc107">$($c.NeedsReboot.ToString("N0")</div><div class="label">Потребує перезавантаження><2 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.NeedsReboot/$c.Total)*100,1)}інакше{0})% – очікує перезавантаження><6 /div></a><9     <class="card" href="#s-upd-pend" onclick="openSection('d-upd-pend')" style="border-left-color:#6f42c1; text-decoration:none"><div class="value" style="color:#6f42c1">$($c.UpdatePending.ToString("N0"))</div><div class="label">Update Pending</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.UpdatePending/$c.Total)*100,1)}else{0})% – політика/WinCS застосовано, очікує оновлення><2 /div></a><5     <class="card" href="#s-rip" onclick="openSection('d-rip')" style="border-left-color:#17a2b8; text-decoration:none"><div class="value">$($c.RolloutInProgress)</div><div class="label">Rollout In Progress><4 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.RolloutInProgress/$c.Total)*100,1)}інакше{0})%</div></a><11     <class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#28a745; text-decoration:none"><div class="value" style="color:#28a745">$($c.HighConf.ToString("N0")</div><div class="label">High Confidence><20 /div><div class="pct">$($stats. PercentHighConfidence)% – безпечне для розгортання><24 /div></a><27     <class="card" href="#s-uo" onclick="openSection('d-uo')" style="border-left-color:#17a2b8; text-decoration:none"><div class="value" style="color:#ffc107"><1 $($c.UnderObs.ToString("N0"))><2 /div><div class="label"><5 Under Observation><36 /div><div class="pct"><9 $(if($c.Total -gt 0){[math]::Round(($c.UnderObs/$c.Total)*100,1)}інакше{0})%</div></a><3     <class="card" href="#s-ar" onclick="openSection('d-ar')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.ActionReq.ToString("N0"))</div><div class="label">Action Required><2 /div><div class="pct">$($stats. PercentActionRequired)% – потрібно перевірити><6 /div></a><9     <class="card" href="#s-err" onclick="openSection('d-err')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($stAtRisk.ToString("N0")</div><div class="label">At Risk><68 /div><div class="pct">$($stats. PercentAtRisk)% – схоже на невдале><2 /div></a><5     <class="card" href="#s-td" onclick="openSection('d-td')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.TaskDisabled.ToString("N0")</div><div class="label">Task Disabled><4 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TaskDisabled/$c.Total)*100,1)}else{0})% – заблоковано><8 /div></a><91     <class="card" href="#s-tf" onclick="openSection('d-tf')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.TempPaused.ToString("N0"))</div><div class="label">Temp. Призупинено</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TempPaused/$c.Total)*100,1)}інакше{0})%</div></a>     <class="card" href="#s-ki" onclick="openSection('d-ki')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.WithKnownIssues.ToString("N0")</div><div class="label">Відомі проблеми><6 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.WithKnownIssues/$c.Total)*100,1)}else{0})%</div></a><3     <class="card" href="#s-kek" onclick="openSection('d-kek')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.WithMissingKEK.ToString("N0")</div><div class="label">Missing KEK</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.WithMissingKEK/$c.Total)*100,1)}else{0})%</div></a>     <class="card" href="#s-err" onclick="openSection('d-err')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.WithErrors.ToString("N0"))</div><div class="label">With Errors</div><div class="pct"><1 $($stats. PercentWithErrors)% – помилки UEFI</div></a>     ><6 class="card" href="#s-tf" onclick="openSection('d-tf')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545"><9 $($c.TempFailures.ToString("N0"))</div><div class="label">Temp. Помилки</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TempFailures/$c.Total)*100,1)}інакше{0})%</div></a>     <class="card" href="#s-pf" onclick="openSection('d-pf')" style="border-left-color:#721c24; text-decoration:none"><div class="value" style="color:#721c24">$($c.PermFailures.ToString("N0")</div><div class="label">Not Supported><6 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.PermFailures/$c.Total)*100,1)}інакше{0})%</div></a><3 </div>

<!-- швидкість розгортання & завершення терміну дії Cert –-> <div id="s-velocity" style="display:grid; grid-template-columns:1fr 1fr; gap:20px; margin:15px 0"> <div class="section" style="margin:0">     <h2>&#128197; Швидкість розгортання</h2>     ><2 div class="section-body open"><3         ><4 div style="font-size:2.5em; font-weight:700; color:#28a745"><5 $($c.Updated.ToString("N0")><6 /div>         ><8 div style="color:#666"><9 пристрої оновлено з $($c.Total.ToString("N0")</div>         <div style="margin:10px 0; тло:#e8eaf6; висота:20px; радіус межі:10px; переповнення:hidden"><div style="background:#28a745; висота:100%; ширина:$($stats. PercentCertUpdated)%; радіус межі:10px"></div></div>         <div style="font-size:.8em; color:#888">$($stats. PercentCertUpdated)% виконання</div>         <div style="margin-top:10px; заповнення:10px; фон:#f8f9fa; радіус межі:8px; font-size:.85em">             <div><сильні>Залишилося:</strong> $($stNotUptodate.ToString("N0")) пристроям потрібні дії</div>             <div><сильний>Blocking:</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated) devices (errors + permanent + task disabled)</div>             <div><надійні пристрої>Safe to deploy:</strong> $($stSafeList.ToString("N0")) пристрої (такий самий блок, що й успішний)</div>             $velocityHtml         </div>     </div> </div> <div class="section" style="margin:0; межа зліва:4px суцільна #dc3545">     <h2 style="color:#dc3545">&#9888; Зворотний відлік терміну дії сертифіката</h2>     <div class="section-body open">         <div style="display:flex; проміжок: 15 пікселів; margin-top:10px">             <div style="text-align:center; заповнення:15px; радіус межі:8px; мінімальна ширина:120px; background:linear-gradient(135deg,#fff5f5,#ffe0e0); межа:2px суцільна #dc3545; flex:1">                 <div style="font-size:.65em; color:#721c24; text-transform:uppercase; font-weight:bold">&#9888; FIRST TO EXPIRE</div>                 ><4 div style="font-size:.85em; font-weight:bold; color:#dc3545; margin:3px 0"><5 KEK CA 2011</div>                 ><8 div id="daysKek" style="font-size:2.5em; font-weight:700; color:#dc3545; висота лінії:1"><9 $daysToKek><0 /div><1                 <div style="font-size:.8em; color:#721c24">днів (24 червня 2026 р.)</div><5             </div><7             <div style="text-align:center; заповнення:15px; радіус межі:8px; мінімальна ширина:120px; background:linear-gradient(135deg,#fffef5,#fff3cd); межа:2px суцільна #ffc107; flex:1">                 ><00 div style="font-size:.65em; color:#856404; text-transform:uppercase; font-weight:bold"><01 UEFI CA 2011</div>                 ><04 div id="daysUefi" style="font-size:2.2em; font-weight:700; color:#856404; висота лінії:1; margin:5px 0"><05 $daysToUefi</div>                 ><08 div style="font-size:.8em; color:#856404"><09 днів (27 червня 2026 р.)><10 /div>             ><12 /div>             ><14 div style="text-align:center; заповнення:15px; радіус межі:8px; мінімальна ширина:120px; background:linear-gradient(135deg,#f0f8ff,#d4edff); межа:2px суцільна #0078d4; flex:1"><15                 ><16 div style="font-size:.65em; color:#0078d4; text-transform:uppercase; font-weight:bold"><17 Windows PCA</div>                 ><20 div id="daysPca" style="font-size:2.2em; font-weight:700; color:#0078d4; висота лінії:1; margin:5px 0"><21 $daysToPca><2 /div><3                 ><24 div style="font-size:.8em; color:#0078d4"><25 днів (19 жовтня 2026 р.)><26 /div><7             ><28 /div><9         ><30 /div><1         ><32 div style="margin-top:15px; заповнення:10px; тло:#f8d7da; радіус межі:8px; font-size:.85em; межа зліва:4px суцільна #dc3545"><33             ><34 сильний>&#9888; CRITICAL:><37 /strong> Усі пристрої потрібно оновити до завершення терміну дії сертифіката. Пристрої, не оновлені до крайнього терміну, не можуть застосовувати майбутні оновлення системи безпеки для диспетчера завантаження та безпечного завантаження після завершення терміну дії.</div>     </div> </div> </div>

<!-- діаграми – > <div class="charts">     <div class="chart-box"><h3>Стан розгортання</h3><canvas id="deployChart" height="200"></canvas></div><5     <div class="chart-box"><h3><9 $mfrChartTitle</h3><canvas id="mfrChart" height="200"></canvas></div> </div>

$(якщо ($historyData.Count -ge 1) { "<!-- діаграма історичних тенденцій – > <div class='section'>     <h2 onclick='"toggle('d-trend')'">&#128200; Оновлення перебігу виконання з часом <href='<class='top-link' href='#'>&#8593; top</a></h2>     <div id='d-trend' class='section-body open'>         <canvas id='trendChart' height='120'></canvas>         <div style='font-size:.75em; color:#888; margin-top:5px'>Суцільні лінії = фактичні дані$(якщо ($historyData.Count -ge 2) { " | Пунктирна лінія = прогнозована (експоненційне подвоєння: 2&#x2192;4&#x2192;8&#x2192;16... пристроїв на хвилю)" } інакше { " | Знову запустіть агрегацію завтра, щоб переглянути лінії тренду та проекцію" })</div>     </div> </div>" })

<!-- завантаження CSV --> <div class="section">     <h2 onclick="toggle('dl-csv')">&#128229; Завантажте повні дані (CSV для Excel) <class="top-link" href="#">top</a></h2>     <div id="dl-csv" class="section-body open" style="display:flex; згин-обмотка:обмотка; gap:5px">         <a href="SecureBoot_not_updated_$timestamp.csv" style="display:inline-block; тло:#dc3545; color:#fff; заповнення:6px 14px; радіус межі:5px; text-decoration:none; font-size:.8em">Not Updated ($($stNotUptodate.ToString("N0"))</a>         <a href="SecureBoot_errors_$timestamp.csv" style="display:inline-block; тло:#dc3545; color:#fff; заповнення:6px 14px; радіус межі:5px; text-decoration:none; font-size:.8em"помилки >($($c.WithErrors.ToString("N0"))</a><2         <a href="SecureBoot_action_required_$timestamp.csv" style="display:inline-block; тло:#fd7e14; color:#fff; заповнення:6px 14px; радіус межі:5px; text-decoration:none; font-size:.8em">потрібна дія ($($c.ActionReq.ToString("N0"))</a><6         <a href="SecureBoot_known_issues_$timestamp.csv" style="display:inline-block; тло:#dc3545; color:#fff; заповнення:6px 14px; радіус межі:5px; text-decoration:none; font-size:.8em">відомі проблеми ($($c.WithKnownIssues.ToString("N0"))</a>         <a href="SecureBoot_task_disabled_$timestamp.csv" style="display:inline-block; тло:#dc3545; color:#fff; заповнення:6px 14px; радіус межі:5px; text-decoration:none; font-size:.8em">Завдання вимкнуто ($($c.TaskDisabled.ToString("N0"))</a>         <a href="SecureBoot_updated_devices_$timestamp.csv" style="display:inline-block; тло:#28a745; color:#fff; заповнення:6px 14px; радіус межі:5px; text-decoration:none; font-size:.8em">Оновлено ($($c.Updated.ToString("N0"))</a>         <a href="SecureBoot_Summary_$timestamp.csv" style="display:inline-block; тло:#6c757d; color:#fff; заповнення:6px 14px; радіус межі:5px; text-decoration:none; font-size:.8em">Summary</a>         <div style="width:100%; font-size:.75em; color:#888; margin-top:5px">CSV-файли відкриваються у програмі Excel. Доступно, якщо його розміщено на веб-сервері.</div>     </div> </div>

Підрозділ виробника <!-- --> <div class="section">     <h2 onclick="toggle('mfr')">By Manufacturer <class="top-link" href="#">Top</a></h2><1     <div id="mfr" class="section-body open">     <таблиці><><tr><><1 виробник><2 /th><><5 Усього><6 /th><><9 Оновлено><9><0 /th><><3 Висока впевненість><4 /th><><7 я дія Необхідна><8 /th></tr></thead><3     <><5 $mfrTableRows><6 /tbody></table><9     </div><1 </div>

розділи <!-- пристроїв (перші 200 вбудованих і csv-завантажень) - > <div class="section" id="s-err">     <h2 onclick="toggle('d-err')">&#128308; Пристрої з помилками ($($c.WithErrors.ToString("N0"))) <class="top-link" href="#">&#8593; top</a></h2>     <div id="d-err" class="section-body">$tblErrors</div> </div> <div class="section" id="s-ki">     <h2 onclick="toggle('d-ki')" style="color:#dc3545">&#128308; Відомі проблеми ($($c.WithKnownIssues.ToString("N0"))) <class="top-link" href="#">&#8593; top</a></h2>     <div id="d-ki" class="section-body">$tblKI</div> </div> <div class="section" id="s-kek">     <h2 onclick="toggle('d-kek')">&#128992; Відсутній KEK – подія 1803 ($($c.WithMissingKEK.ToString("N0"))) <class="top-link" href="#">&#8593; top</a></h2>     >&#8593; 0 div id="d-kek" class="section-body">&#8593; 1 $tblKEK</div> >&#8593; 4 /div> >&#8593; 6 div class="section" id="s-ar">&#8593; 7     >&#8593; 8 h2 onclick="toggle('d-ar')" style="color:#fd7e14">&#128992; Потрібна дія ($($c.ActionReq.ToString("N0"))) <class="top-link" href="#">&#8593; top><4 /a></h2><7     <div id="d-ar" class="section-body">$tblActionReq</div> </div> <div class="section" id="s-uo">     <h2 onclick="toggle('d-uo')" style="color:#17a2b8">&#128309; У розділі Спостереження ($($c.UnderObs.ToString("N0")) <class="top-link" href="#">&#8593; top</a></h2>     <div id="d-uo" class="section-body">$tblUnderObs</div> </div> <div class="section" id="s-nu">     <h2 onclick="toggle('d-nu')" style="color:#dc3545">&#128308; Не оновлено ($($stNotUptodate.ToString("N0"))) <class="top-link" href="#">&#8593; top</a></h2>     <div id="d-nu" class="section-body">$tblNotUpd</div> </div> >&#8593; 0 div class="section" id="s-td">&#8593; 1     >&#8593; 2 h2 onclick="toggle('d-td')" style="color:#dc3545">&#128308; Завдання вимкнуто ($($c.TaskDisabled.ToString("N0"))) >&#8593; 5 class="top-link" href="#">&#8593; top</a></h2><1     <div id="d-td" class="section-body">$tblTaskDis><4 /div><5 </div><7 <div class="section" id="s-tf">     <h2 onclick="toggle('d-tf')" style="color:#dc3545">&#128308; Тимчасові помилки ($($c.TempFailures.ToString("N0"))) <class="top-link" href="#">&#8593; top</a></h2>     <div id="d-tf" class="section-body">$tblTemp</div> </div> <div class="section" id="s-pf">     <h2 onclick="toggle('d-pf')" style="color:#721c24">&#128308; Постійні помилки / Не підтримується ($($c.PermFailures.ToString("N0"))) <class="top-link" href="#">&#8593; top</a></h2>     <div id="d-pf" class="section-body">$tblPerm</div> </div> <div class="section" id="s-upd-pend">     <h2 onclick="toggle('d-upd-pend')" style="color:#6f42c1">&#9203; Очікування оновлення ($($c.UpdatePending.ToString("N0"))) – політика/WinCS Applied, Очікується оновлення <class="top-link" href="#">&#8593; top</a></h2>     <div id="d-upd-pend" class="section-body"><p style="color:#666; margin-bottom:10px">Devices where AvailableUpdatesPolicy або WinCS key is applied but UEFICA2023Status is still NotStarted, InProgress або null.</p>$tblUpdatePending</div> </div> <div class="section" id="s-rip">     <h2 onclick="toggle('d-rip')" style="color:#17a2b8">&#128309; Виконується розгортання ($($c.RolloutInProgress.ToString("N0"))) <class="top-link" href="#">&#8593; top</a></h2>     <div id="d-rip" class="section-body">$tblRolloutIP</div> </div> <div class="section" id="s-sboff">     <h2 onclick="toggle('d-sboff')" style="color:#6c757d">&#9899; SecureBoot OFF ($($c.SBOff.ToString("N0"))) - з області <class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-sboff" class="section-body">$tblSBOff</div> </div> <div class="section" id="s-upd">     <h2 onclick="toggle('d-upd')" style="color:#28a745">&#128994; Оновлені пристрої ($($c.Updated.ToString("N0"))) <class="top-link" href="#">&#8593; top</a></h2>     <div id="d-upd" class="section-body">$tblUpdated</div> </div> <div class="section" id="s-nrb">     <h2 onclick="toggle('d-nrb')" style="color:#ffc107">&#128260; Оновлено – потребує перезавантаження ($($c.NeedsReboot.ToString("N0"))) <class="top-link" href="#">&#8593; top</a></h2>     <div id="d-nrb" class="section-body">$tblNeedsReboot</div> </div>

<div class="footer">приладну дошку розгортання сертифіката безпечного завантаження | Створено $($stats. ReportGeneratedAt) | StreamingMode | Максимальна пам'ять: ${stPeakMemMB} МБ</div> </div><!-- /container -->

>сценаріїв < перемикач(ідентифікатор){var e=document.getElementById(id); e.classList.toggle('open')} openSection(id){var e=document.getElementById(id); if(e&&!e.classList.contains('open')){e.classList.add('open')}} new Chart(document.getElementById('deployChart'),{type:'doughnut',data:{labels:['Updated','Update Pending','High Confidence','Under Observation','Action Required','Temp. Призупинено","Не підтримується","SecureBoot OFF","З помилками"],набори даних:[{data:[$($c.Updated);$($c.UpdatePending);$($c.HighConf);$($c.UnderObs);$($c.ActionReq);$($c.TempPaused);$($c.NotSupported);$($c.SBOff);$($c.SBOff);$($c.WithErrors)],backgroundColor:['#28a745','#6f42c1','#20c997','#17a2b8','#fd7e14','#6c757d'', #721c24","#adb5bd","#dc3545"]}]},options:{responsive:true,plugins:{legend:{position:'right',labels:{font:{size:11}}}}}}); new Chart(document.getElementById('mfrChart'),{type:'bar',data:{labels:[$mfrLabels],datasets:[{label:'Updated',data:[$mfrUpdated],backgroundColor:'#28a745'};{label:'Update Pending',data:[$mfrUpdatePending],backgroundColor:'#6f42c1'},{label:'High Confidence',data:[$mfrHighConf],backgroundColor:'#20c997'},{label:'Under Observation',data:[$mfrUnderObs],backgroundColor:'#17a2b8'},{label:'Action Required',data:[$mfrActionReq],backgroundColor:'#fd7e14'},{ label:'Temp. Paused',data:[$mfrTempPaused],backgroundColor:'#6c757d'},{label:'Not Supported',data:[$mfrNotSupported],backgroundColor:'#721c24'},{label:'SecureBoot OFF',data:[$mfrSBOff],backgroundColor:'#adb5bd'}{label:'With Errors',data:[$mfrWithErrors],backgroundColor:'#dc3545'}]},options:{responsive:true,scales:{x:{stacked:true},y:{stacked:true}},plugins:{legend:{position:'top'}}}); Діаграма тренду за минулий період якщо (document.getElementById('trendChart')) { var allLabels = [$allChartLabels]; var actualUpdated = [$trendUpdated]; var actualNotUpdated = [$trendNotUpdated]; var actualTotal = [$trendTotal]; var projData = [$projDataJS]; var projNotUpdData = [$projNotUpdJS]; var histLen = actualUpdated.length; var projLen = projData.length; var paddedUpdated = actualUpdated.concat(Array(projLen).fill(null)); var paddedNotUpdated = actualNotUpdated.concat(Array(projLen).fill(null)); var paddedTotal = actualTotal.concat(Array(projLen).fill(null)); var projLine = Array(histLen).fill(null); var projNotUpdLine = Array(histLen).fill(null); якщо (projLen > 0) { projLine[histLen-1] = actualUpdated[histLen-1]; projLine = projLine.concat(projData); projNotUpdLine[histLen-1] = actualNotUpdated[histLen-1]; projNotUpdLine = projNotUpdLine.concat(projNotUpdAta); } var datasets = [     {label:'Updated',data:paddedUpdated,borderColor:'#28a745',backgroundColor:'rgba(40,167,69,0.1)',fill:true,tension:0.3,borderWidth:2},     {label:'Not Updated',data:paddedNotUpdated,borderColor:'#dc3545',backgroundColor:'rgba(220,53,69,0.1)',fill:true,tension:0.3,borderWidth:2},     {label:'Total',data:paddedTotal,borderColor:'#6c757d',borderDash:[5,5],fill:false,tension:0,pointRadius:0,borderWidth:1} ]; якщо (projLen > 0) {     datasets.push({label:'Projected Updated (подвоєння 2x)',data:projLine,borderColor:'#28a745',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'});     datasets.push({label:'Projected Not Updated',data:projNotUpdLine,borderColor:'#dc3545',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}); } (}) new Chart(document.getElementById('trendChart'),{type:'line',data:{labels:allLabels,datasets:datasets},options:{responsive:true,scales:{y:{beginAtZero:true,title:{display :true,text:'Devices'}},x:{title:{display:true,text:'Date'}},plugins:{legend:{position:'top'},title:{display:true,text:'Secure Boot Update Progress Over Time'}}}); } (}) Динамічний зворотний відлік (function(){var t=new Date();k=new Date('2026-06-24'),u=new Date('2026-06-27'),p=new Date('2026-10-19'); var dk=document.getElementById('daysKek'),du=document.getElementById('daysUefi'),dp=document.getElementById('daysPca'); if(dk)dk.textContent=Math.max(0,Math.ceil((k-t)/864e5)); if(du)du.textContent=Math.max(0,Math.ceil((u-t)/864e5)); if(dp)dp.textContent=Math.max(0;Math.ceil((p-t)/864e5))}))();$endScript $endBody $endHtml "@         [System.IO.File]::WriteAllText($htmlPath, $htmlContent, [System.Text.UTF8Encoding]::new($false))         # Завжди зберігайте стабільну копію "Найновіша", щоб адміністраторам не потрібно було відстежувати позначки часу         $latestPath = Join-Path $OutputPath "SecureBoot_Dashboard_Latest.html"         Copy-Item $htmlPath $latestPath -Force         $stTotal = $streamSw.Elapsed.TotalSeconds         # Зберегти маніфест файлу для інкрементного режиму (швидке виявлення без змін під час наступного запуску)         якщо ($IncrementalMode -або $StreamingMode) {             $stManifestDir = Join-Path $OutputPath ".cache"             if (-not (test-path $stManifestDir)) { New-Item -ItemType Directory -Path $stManifestDir -Force | Out-Null }             $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json"             $stNewManifest = @{}             Write-Host "Збереження маніфесту файлу в інкрементному режимі..." -ForegroundColor Gray             foreach ($jf in $jsonFiles) {                 $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{                     LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o")                     Розмір = $jf. Довжина                 } (})             } (})             Save-FileManifest – $stNewManifest маніфесту – шлях $stManifestPath             Write-Host " Збережений маніфест для файлів $($stNewManifest.Count)" -ForegroundColor DarkGray         } (})         # ОЧИЩЕННЯ ЗБЕРЕЖЕННЯ         # Orchestrator повторного використання папки (Aggregation_Current): зберегти тільки останню версію запуску (1)         # Admin вручну запускається / інші папки: зберегти останні 7 запусків         # Зведені CSVs НІКОЛИ не видаляються – вони крихітні (~1 КБ) і є джерелом резервної копії для журналу тренду         $outputLeaf = Split-Path $OutputPath -Листок         $retentionCount = якщо ($outputLeaf -eq 'Aggregation_Current') { 1 } інакше { 7 }         # Префікси файлів, безпечні для очищення (ефемерні знімки для кожного запуску)         $cleanupPrefixes = @(             "SecureBoot_Dashboard_",             "SecureBoot_action_required_",             "SecureBoot_ByManufacturer_",             "SecureBoot_ErrorCodes_",             "SecureBoot_errors_",             "SecureBoot_known_issues_",             "SecureBoot_missing_kek_",             "SecureBoot_needs_reboot_",             "SecureBoot_not_updated_",             "SecureBoot_secureboot_off_",             "SecureBoot_task_disabled_",             "SecureBoot_temp_failures_",             "SecureBoot_perm_failures_",             "SecureBoot_under_observation_",             "SecureBoot_UniqueBuckets_",             "SecureBoot_update_pending_",             "SecureBoot_updated_devices_",             "SecureBoot_rollout_inprogress_",             "SecureBoot_NotUptodate_",             "SecureBoot_Kusto_"         )         # Пошук усіх унікальних позначок часу лише з файлів, які можна очистити         $cleanableFiles = Get-ChildItem $OutputPath -File -EA SilentlyContinue |             Where-Object { $f = $_. Ім'я; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Кількість -gt 0 }         $allTimestamps = @($cleanableFiles | ForEach-Object {             якщо ($_. Name -match '(\d{8}-\d{6})') { $Matches[1] }         } | Sort-Object –унікальний –за спаданням)         якщо ($allTimestamps.Count -gt $retentionCount) {             $oldTimestamps = $allTimestamps | Select-Object – пропустити $retentionCount             $removedFiles = 0; $freedBytes = 0             foreach ($oldTs у $oldTimestamps) {                 foreach ($prefix in $cleanupPrefixes) {                     $oldFiles = Get-ChildItem $OutputPath -File -Filter "${prefix}${oldTs}*" -EA SilentlyContinue                     foreach ($f in $oldFiles) {                         $freedBytes += $f.Довжина                         Remove-Item $f.FullName -Force -EA SilentlyContinue                         $removedFiles++                     } (})                 } (})             } (})             $freedMB = [math]::Round($freedBytes / 1MB, 1)             Write-Host "Очищення збереження: видалено $removedFiles файли зі старих запусків $($oldTimestamps.Count), вивільнено ${freedMB} МБ (збереження останніх $retentionCount + всі зведення/notUptodate CSVs)" -ForegroundColor DarkGray         } (})         Write-Host "'n$("=" * 60)" -ForegroundColor Cyan         Write-Host "ПОТОКОВЕ АГРЕГУВАННЯ ЗАВЕРШЕНО" - Зелений колір переднього плану         Write-Host ("=" * 60) -ForegroundColor Cyan         Write-Host " Усього пристроїв: $($c.Total.ToString("N0"))" -ForegroundColor White         Write-Host " NOT UPDATED: $($stNotUptodate.ToString("N0")) ($($stats. PercentNotUptodate)%)" -ForegroundColor $(якщо ($stNotUptodate -gt 0) { "Жовтий" } ще { "Зелений" })         Write-Host " Оновлено: $($c.Updated.ToString("N0")) ($($stats. PercentCertUpdated)%)" -ForegroundColor Green         Write-Host " з помилками: $($c.WithErrors.ToString("N0"))" -ForegroundColor $(if ($c.WithErrors -gt 0) { "Red" } else { "Green" })         Write-Host "Максимальна пам'ять: ${stPeakMemMB} МБ" -ForegroundColor Cyan         Write-Host " Час: $([math]::Round($stTotal/60,1)) min" -ForegroundColor White         Write-Host " Dashboard: $htmlPath" -ForegroundColor White         return [PSCustomObject]$stats     } (})     #ENDREGION РЕЖИМ ПОТОКОВОГО ПЕРЕДАВАННЯ } інакше {     Write-Error "Шлях вводу не знайдено: $InputPath"     вихід 1 }                                                      

Потрібна додаткова довідка?

Потрібні додаткові параметри?

Ознайомтеся з перевагами передплати, перегляньте навчальні курси, дізнайтесь, як захистити свій пристрій тощо.