QUAN TRỌNG Bài viết này có chứa tập lệnh mẫu này đã bị gỡ bỏ. Bắt đầu với các bản cập nhật Windows được phát hành vào và sau ngày 12 tháng 5 năm 2026, tập lệnh mẫu nằm trong thư mục %systemroot%\SecureBoot\ExampleRolloutScripts trên thiết bị của bạn.

Sao chép và dán tập lệnh mẫu này và sửa đổi khi cần thiết cho môi trường của bạn:

<# . TÓM TẮT     Tổng hợp dữ liệu trạng thái Khởi động An toàn JSON từ nhiều thiết bị vào báo cáo tóm tắt.

. MÔ TẢ     Đọc các tệp JSON trạng thái Khởi động An toàn được thu thập và tạo ra:     - Bảng điều khiển HTML với biểu đồ và lọc     - Tóm tắt theo ConfidenceLevel     - Phân tích bộ chứa thiết bị duy nhất cho chiến lược thử nghiệm          Hỗ trợ:     - Tệp trên mỗi máy: HOSTNAME_latest.json (khuyến cáo)     - Tệp JSON đơn          Tự động deduplicates bởi HostName, giữ collectionTime mới nhất.     Theo mặc định, chỉ bao gồm các thiết bị có độ tin cậy "Phản hồi Hành động" hoặc "Cao"     để tập trung vào bộ chứa hữu dụng. Sử dụng -IncludeAllConfidenceLevels để ghi đè.

. PARAMETER InputPath     Đường dẫn đến (các) tệp JSON:     - Thư mục: Đọc tất cả các tệp *_latest.json (hoặc *.json tệp nếu không có tệp _latest)     - File: Đọc tệp JSON đơn

. PARAMETER OutputPath     Đường dẫn cho báo cáo đã tạo (mặc định: .\SecureBootReports)

. VÍ DỤ     # Tổng hợp từ thư mục của tệp trên mỗi máy (được khuyến nghị)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$"     # Đọc: \\contoso\SecureBootLogs$\*_latest.json

. VÍ DỤ     # Vị trí đầu ra tùy chỉnh     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -OutputPath "C:\Reports\SecureBoot"

. VÍ DỤ     # Chỉ bao gồm Hành động Req và Độ tin cậy cao (hành vi mặc định)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$"     # Excludes: Observation, Paused, Not Supported

. VÍ DỤ     # Bao gồm tất cả các mức tin cậy (ghi đè bộ lọc)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeAllConfidenceLevels

. VÍ DỤ     # Bộ lọc mức tin cậy tùy chỉnh     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeConfidenceLevels @("Phản hồi Hành động", "Cao", "Quan sát")

. VÍ DỤ     # QUY MÔ DOANH NGHIỆP: Chế độ tăng dần - chỉ các tệp đã thay đổi quy trình (chạy nhanh theo sau)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode     # Chạy lần đầu: Tải đầy ~ 2 giờ cho các thiết bị 500K     # Chạy tiếp theo: Giây nếu không có thay đổi, phút cho deltas

. VÍ DỤ     # Skip HTML if nothing changed (fastest for monitoring)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode -SkipReportIfUnchanged     # Nếu không có tệp nào thay đổi kể từ lần chạy cuối: ~5 giây

. VÍ DỤ     # Chế độ chỉ tóm tắt - bỏ qua các bảng thiết bị lớn (1-2 phút so với 20 phút trở lên)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -SummaryOnly     # Tạo CSV nhưng bỏ qua bảng điều khiển HTML với bảng thiết bị đầy đủ

. GHI CHÚ     Ghép nối với Detect-SecureBootCertUpdateStatus.ps1 triển khai doanh nghiệp.Xem mục GPO-DEPLOYMENT-GUIDE.md hướng dẫn triển khai đầy đủ.     Hành vi mặc định loại trừ các thiết bị Quan sát, Đã tạm dừng và Không Được hỗ trợ     để chỉ tập trung báo cáo vào bộ chứa thiết bị hữu dụng.#>

param(     [Parameter(Mandatory = $true)]     [chuỗi]$InputPath,          [Parameter(Mandatory = $false)]     [string]$OutputPath = ".\SecureBootReports",          [Parameter(Mandatory = $false)]     [string]$ScanHistoryPath = ".\SecureBootReports\ScanHistory.json",          [Parameter(Mandatory = $false)]     [string]$RolloutStatePath, # Path to RolloutState.json identify InProgress devices          [Parameter(Mandatory = $false)]     [string]$RolloutSummaryPath, # Path to SecureBootRolloutSummary.json from Orchestrator (contains projection data)          [Parameter(Mandatory = $false)]     [string[]]$IncludeConfidenceLevels = @("Action Bắt buộc", "Độ tin cậy Cao"), # Chỉ bao gồm các mức tin cậy này (mặc định: bộ chứa có thể thực thi)          [Parameter(Mandatory = $false)]     [switch]$IncludeAllConfidenceLevels, # Ghi đè bộ lọc để bao gồm tất cả các mức tin cậy          [Parameter(Mandatory = $false)]     [switch]$SkipHistoryTracking,          [Parameter(Mandatory = $false)]     [switch]$IncrementalMode, # Enable delta processing - only load changed files since last run          [Parameter(Mandatory = $false)]     [chuỗi]$CachePath, # Đường dẫn đến thư mục bộ đệm ẩn (mặc định: OutputPath\.cache)          [Parameter(Mandatory = $false)]     [int]$ParallelThreads = 8, # Số chuỗi hội thoại song song để tải tệp (PS7+)          [Parameter(Mandatory = $false)]     [switch]$ForceFullRefresh, # Bắt buộc tải lại đầy đủ ngay cả trong chế độ gia tăng          [Parameter(Mandatory = $false)]     [switch]$SkipReportIfUnchanged, # Skip HTML/CSV generation if no files changed (just output stats)          [Parameter(Mandatory = $false)]     [switch]$SummaryOnly, # Chỉ tạo số liệu thống kê tóm tắt (không có bảng thiết bị lớn) - nhanh hơn nhiều          [Parameter(Mandatory = $false)]     [switch]$StreamingMode # Memory-efficient mode: process chunks, write CSVs incrementally, only summaries in memory )

# Tự sửa chữa: Dải ký tự Unicode vô hình được tiêm bởi CMS web khi sao chép dán từ các bài viết HTML.# CmS support.microsoft.com các khoảng trắng (U+200B), Khoảng trắng Không Ngắt (U+00A0) và các khoảng trắng khác # các ký tự ẩn xung quanh thẻ html bên trong chuỗi này, gây ra lỗi phân tích cú pháp PowerShell.if ($MyInvocation.MyCommand.Path) {     $rawScript = [System.IO.File]::ReadAllText($MyInvocation.MyCommand.Path)     if ($rawScript -match '[\u200B-\u200F\uFEFF]' -or $rawScript -match '\xA0') {         Write-Host "CẢNH BÁO: Phát hiện ký tự Unicode vô hình (có thể từ web copy-paste) - tập lệnh tự động làm sạch..." -ForegroundColor Yellow         $cleaned = $rawScript '[\u200B-\u200F\uFEFF]', ''         $cleaned = $cleaned -replace '\xA0', ' '         [System.IO.File]::WriteAllText($MyInvocation.MyCommand.Path, $cleaned, [System.Text.UTF8Encoding]::new($false))         Write-Host "Đã dọn sạch tập lệnh thành công. Re-launching..." -ForegroundColor Green         & $MyInvocation.MyCommand.Path @PSBoundParameters         thoát $LASTEXITCODE     } }

# Tự động nâng lên PowerShell 7 nếu khả dụng (nhanh hơn 6x đối với tập dữ liệu lớn) if ($PSVersionTable.PSVersion.Major -lt 7) {     $pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source     nếu ($pwshPath) {         Write-Host phát hiện "PowerShell $($PSVersionTable.PSVersion) - khởi chạy lại với PowerShell 7 để xử lý nhanh hơn..." -ForegroundColor Yellow         # Xây dựng lại danh sách đối số từ tham số liên kết         $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bỏ qua', '-Tệp', $MyInvocation.MyCommand.Path)         foreach ($key trong $PSBoundParameters.Keys) {             $val = $PSBoundParameters[$key]             if ($val -is [switch]) {                 nếu ($val. IsPresent) { $relaunchArgs += "-$key" }             } elseif ($val -is [array]) {                 $relaunchArgs += "-$key"                 $relaunchArgs += ($val -join ',')             } người khác {                 $relaunchArgs += "-$key"                 $relaunchArgs += "$val"             }         }         & $pwshPath @relaunchArgs         thoát $LASTEXITCODE     } }

$ErrorActionPreference = "Tiếp tục" $timestamp = Get-Date -Format "yyyyMdd-HHmmss" $scanTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Triển khai và Giám sát Mẫu"

# Lưu ý: Kịch bản này không có phụ thuộc vào các kịch bản khác.# Đối với bộ công cụ hoàn chỉnh, tải về từ: $DownloadUrl -> $DownloadSubPage

#region lập Write-Host "=" * 60 -ForegroundColor Cyan Write-Host "Tập hợp dữ liệu khởi động an toàn" -ForegroundColor Cyan Write-Host "=" * 60 -ForegroundColor Cyan

# Tạo thư mục đầu ra if (-not (Test-Path $OutputPath)) {     New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null }

# Tải dữ liệu - hỗ trợ định dạng CSV (thừa tự) và JSON (gốc) Write-Host "'nLoading data from: $InputPath" -ForegroundColor Yellow

# Helper function to normalize device object (handle field name differences) hàm Normalize-DeviceRecord {     param($device)          # Handle Hostname vs HostName (JSON sử dụng Hostname, CSV sử dụng HostName)     nếu ($device. PSObject.Properties['Hostname'] -and -not $device. PSObject.Properties['HostName']) {         $device | Add-Member -NotePropertyName 'HostName' -NotePropertyValue $device. Hostname -Force     }          # Handle Confidence vs ConfidenceLevel (JSON dùng Confidence, CSV dùng ConfidenceLevel)     # ConfidenceLevel là tên trường chính thức - ánh xạ tự tin với trường đó     nếu ($device. PSObject.Properties['Confidence'] -and -not $device. PSObject.Properties['ConfidenceLevel']) {         $device | Add-Member -NotePropertyName 'ConfidenceLevel' -NotePropertyValue $device. Tin cậy -Force     }          # Theo dõi trạng thái cập nhật qua Event1808Count OR UEFICA2023Status="Đã cập nhật"     # Điều này cho phép theo dõi số lượng thiết bị trong mỗi bộ chứa tin cậy đã được cập nhật     $event 1808 = 0     nếu ($device. PSObject.Properties['Event1808Count']) {         $event 1808 = [int]$device. Sự kiện1808Count     }     $uefiCaUpdated = $false     nếu ($device. PSObject.Properties['UEFICA2023Status'] -and $device. UEFICA2023Status -eq "Đã cập nhật") {         $uefiCaUpdated = $true     }          nếu ($event 1808 -gt 0 -hoặc $uefiCaUpdated) {         # Đánh dấu là đã cập nhật cho bảng điều khiển/lô-gic triển khai - nhưng KHÔNG ghi đè ConfidenceLevel         $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $true -Force     } người khác {         $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $false -Force                  # Phân loại Mức tin cậy:         # - "Độ tin cậy cao", "Đang quan sát...", "Tạm dừng tạm dừng...", "Không hỗ trợ..." = sử dụng nguyên bản         # - Mọi thứ khác (null, rỗng, "UpdateType:...", "Không xác định", "N/A") = rơi vào Yêu cầu Hành động trong các bộ đếm         # Không cần chuẩn hóa — nhánh của bộ đếm phát trực tuyến khác xử lý nó     }          # Xử lý OEMManufacturerName vs WMI_Manufacturer (JSON sử dụng OEM*, ứng dụng cũ WMI_*)     nếu ($device. PSObject.Properties['OEMManufacturerName'] -and -not $device. PSObject.Properties['WMI_Manufacturer']) {         $device | Add-Member -NotePropertyName 'WMI_Manufacturer' -NotePropertyValue $device. OEMManufacturerName -Force     }          # Xử lý OEMModelNumber so với WMI_Model     nếu ($device. PSObject.Properties['OEMModelNumber'] -and -not $device. PSObject.Properties['WMI_Model']) {         $device | Add-Member -NotePropertyName 'WMI_Model' -NotePropertyValue $device. OEMModelNumber -Force     }          # Xử lý FirmwareVersion vs BIOSDescription     nếu ($device. PSObject.Properties['FirmwareVersion'] -and -not $device. PSObject.Properties['BIOSDescription']) {         $device | Add-Member -NotePropertyName 'BIOSDescription' -NotePropertyValue $device. FirmwareVersion -Force     }          trả lại $device }

#region xử lý tăng dần / Quản lý Bộ đệm ẩn # Thiết lập đường dẫn bộ đệm ẩn if (-not $CachePath) {     $CachePath = Join-Path $OutputPath ".cache" } $manifestPath = Join-Path $CachePath "FileManifest.json" $deviceCachePath = Join-Path $CachePath "DeviceCache.json"

# Các hàm quản lý bộ đệm ẩn hàm Get-FileManifest {     param([string]$Path)     if (Test-Path $Path) {         hãy thử {             $json = Get-Content $Path -Raw | ConvertFrom-Json             # Chuyển đổi PSObject thành hàm băm (tương thích PS5.1 - PS7 có -AsHashtable)             $ht = @{}             $json. PSObject.Properties | ForEach-Object { $ht[$_. Name] = $_. Giá trị }             trả lại $ht         } bắt {             trả lại @{}         }     }     trả lại @{} }

hàm Save-FileManifest {     param([hashtable]$Manifest, [string]$Path)     $dir = Split-Path $Path -Parent     if (-not (Test-Path $dir)) {         New-Item -ItemType Directory -Path $dir -Force | Out-Null     }     $Manifest | ConvertTo-Json -Chiều sâu 3 -Nén | Set-Content $Path -Force }

hàm Get-DeviceCache {     param([string]$Path)     if (Test-Path $Path) {         hãy thử {             $cacheData = Get-Content $Path -Raw | ConvertFrom-Json             Write-Host " Bộ đệm ẩn thiết bị đã tải: thiết bị $($cacheData.Count) " -ForegroundColor DarkGray             trả lại $cacheData         } bắt {             Write-Host " Cache corrupted, will rebuild" -ForegroundColor Yellow             trả lại @()         }     }     trả lại @() }

hàm Save-DeviceCache {     param($Devices, [string]$Path)     $dir = Split-Path $Path -Parent     if (-not (Test-Path $dir)) {         New-Item -ItemType Directory -Path $dir -Force | Out-Null     }     # Chuyển đổi sang mảng và lưu     $deviceArray = @($Devices)     $deviceArray | ConvertTo-Json -Chiều sâu 10 -Nén | Set-Content $Path -Force     Write-Host " Bộ đệm ẩn thiết bị đã lưu: thiết bị $($deviceArray.Count) " -ForegroundColor DarkGray }

hàm Get-ChangedFiles {     param(         [System.IO.FileInfo[]]$AllFiles,         [hàm băm]$Manifest     )          $changed = [System.Collections.ArrayList]::new()     $unchanged = [System.Collections.ArrayList]::new()     $newManifest = @{}          # Xây dựng tra cứu phân biệt chữ hoa chữ thường từ tệp kê khai (chuẩn hóa thành chữ thường)     $manifestLookup = @{}     foreach ($mk trong $Manifest.Keys) {         $manifestLookup[$mk. ToLowerInvariant()] = $Manifest[$mk]     }          foreach ($file in $AllFiles) {         $key = $file. FullName.ToLowerInvariant() # Chuẩn hóa đường dẫn đến chữ thường         $lwt = $file. LastWriteTimeUtc.ToString("o")         $newManifest[$key] = @{             LastWriteTimeUtc = $lwt             Kích cỡ = $file. Chiều dài         }                  if ($manifestLookup.ContainsKey($key)) {             $cached = $manifestLookup[$key]             nếu ($cached. LastWriteTimeUtc -eq $lwt -and $cached. Kích cỡ -eq $file. Độ dài) {                 [không có hiệu lực]$unchanged. Add($file)                 Tiếp tục             }         }         [void]$changed. Add($file)     }          trả lại @{         Đã thay đổi = $changed         Không đổi = $unchanged         NewManifest = $newManifest     } }

# Tải tệp song song cực nhanh bằng cách sử dụng xử lý theo lô hàm Load-FilesParallel {     param(         [System.IO.FileInfo[]]$Files,         [số liệu int]$Threads = 8     )

    $totalFiles = $Files. Đếm     # Sử dụng các lô ~ 1000 tập tin mỗi để kiểm soát bộ nhớ tốt hơn     $batchSize = [toán học]::Min(1000, [toán học]::Ceiling($totalFiles / [toán học]::Max(1, $Threads)))     $batches = [System.Collections.Generic.List[object]::new()     

    cho ($i = 0; $i -lt $totalFiles; $i += $batchSize) {         $end = [toán học]::Min($i + $batchSize, $totalFiles)         $batch = $Files[$i.. ($end-1)]         $batches. Add($batch)     }     Write-Host " ($($batches. Count) batches of ~$batchSize files each)" -NoNewline -ForegroundColor DarkGray     $flatResults = [System.Collections.Generic.List[object]::new()     # Kiểm tra xem PowerShell 7+ parallel có khả dụng hay không     $canParallel = $PSVersionTable.PSVersion.Major -ge 7     if ($canParallel -and $Threads -gt 1) {         # PS7+: Xử lý các lô song song         $results = $batches | ForEach-Object -ThrottleLimit $Threads -Parallel {             $batchFiles = $_             $batchResults = [System.Collections.Generic.List[object]::new()             foreach ($file in $batchFiles) {                 hãy thử {                     $content = [System.IO.File]::ReadAllText($file. Họ Tên) | ConvertFrom-Json                     $batchResults.Add($content)                 } bắt { }             }             $batchResults.ToArray()         }         foreach ($batch in $results) {             if ($batch) { foreach ($item in $batch) { $flatResults.Add($item) } }         }     } người khác {         # PS5.1 dự phòng: Xử lý tuần tự (vẫn nhanh cho các tệp <10K)         foreach ($file $Files) {             hãy thử {                 $content = [System.IO.File]::ReadAllText($file. Họ Tên) | ConvertFrom-Json                 $flatResults.Add($content)             } bắt { }         }     }     return $flatResults.ToArray() } #endregion                         

$allDevices = @() if (Test-Path $InputPath -PathType Leaf) {     # Tệp JSON đơn     if ($InputPath -like "*.json") {         $jsonContent = Get-Content -Đường dẫn $InputPath -Nguyên | ConvertFrom-Json         $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ }         Write-Host "Đã tải bản ghi $($allDevices.Count) từ tệp"     } người khác {         Write-Error "Chỉ hỗ trợ định dạng JSON. Tệp phải có phần mở .json mở rộng."         thoát 1     } } elseif (Test-Path $InputPath -PathType Container) {     # Thư mục - Chỉ JSON     $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue |                    Where-Object { $_. Name -notmatch "ScanHistory|RolloutState|RolloutPlan" })          # Thích *_latest.json tệp nếu chúng tồn tại (chế độ trên mỗi máy)     $latestJson = $jsonFiles | Where-Object { $_. Name -like "*_latest.json" }     if ($latestJson.Count -gt 0) { $jsonFiles = $latestJson }          $totalFiles = $jsonFiles.Count          if ($totalFiles -eq 0) {         Write-Error "Không tìm thấy tệp JSON nào trong: $InputPath"         thoát 1     }          Write-Host "Tìm thấy $totalFiles tệp JSON" -ForegroundColor Gray          # Hàm Helper để khớp với các mức độ tin cậy (xử lý cả biểu mẫu ngắn và đầy đủ)     # Được xác định sớm để cả StreamingMode và đường dẫn bình thường đều có thể sử dụng     hàm Test-ConfidenceLevel {         param([string]$Value, [string]$Match)         if ([string]::IsNullOrEmpty($Value)) { return $false }         chuyển đổi ($Match) {             "HighConfidence" { return $Value -eq "High Confidence" }             "UnderObservation" { return $Value -like "Under Observation*" }             "ActionRequired" { return ($Value -like "*Action Required*" -or $Value -eq "Action Required") }             "Tạm thờiPaused" { return $Value -like "Temporarily Paused*" }             "NotSupported" { return ($Value -like "Not Supported*" -or $Value -eq "Not Supported") }             { return $false }         }     }          #region STREAMING MODE - Xử lý hiệu quả bộ nhớ cho các tập dữ liệu lớn     # Luôn sử dụng StreamingMode để xử lý hiệu quả bộ nhớ và bảng điều khiển kiểu mới     if (-not $StreamingMode) {         Write-Host "StreamingMode tự động bật (bảng điều khiển kiểu mới)" -ForegroundColor Yellow         $StreamingMode = $true         if (-not $IncrementalMode) { $IncrementalMode = $true }     }          # Khi -StreamingMode được bật, xử lý các tập tin trong khối chỉ giữ bộ đếm trong bộ nhớ.# Dữ liệu cấp thiết bị được ghi vào các tệp JSON theo khúc để tải theo yêu cầu trong bảng điều khiển.# Mức sử dụng bộ nhớ: ~ 1,5 GB bất kể kích thước tập dữ liệu (so với 10-20 GB mà không phát trực tuyến).nếu ($StreamingMode) {         Write-Host "BẬT CHẾ ĐỘ PHÁT TRỰC TUYẾN - xử lý hiệu quả bộ nhớ" -ForegroundColor Green         $streamSw = [System.Diagnostics.Stopwatch]::StartNew()         # KIỂM TRA GIA TĂNG: Nếu không có tệp nào thay đổi kể từ lần chạy gần nhất, hãy bỏ qua quá trình xử lý hoàn toàn         if ($IncrementalMode -and -not $ForceFullRefresh) {             $stManifestDir = Join-Path $OutputPath ".cache"             $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json"             if (Test-Path $stManifestPath) {                 Write-Host "Kiểm tra thay đổi kể từ lần phát trực tuyến cuối cùng chạy..." -ForegroundColor Cyan                 $stOldManifest = Get-FileManifest -Đường dẫn $stManifestPath                 if ($stOldManifest.Count -gt 0) {                     $stChanged = $false                     # Kiểm tra nhanh: cùng một số tệp?                     if ($stOldManifest.Count -eq $totalFiles) {                         # Kiểm tra 100 tệp MỚI NHẤT (sắp xếp theo LastWriteTime giảm dần)                         # Nếu có bất kỳ tệp nào thay đổi, tệp sẽ có dấu thời gian gần đây nhất và xuất hiện đầu tiên                         $sampleSize = [toán học]::Min(100, $totalFiles)                         $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc -Giảm dần | Select-Object -First $sampleSize                         foreach ($sf in $sampleFiles) {                             $sfKey = $sf. FullName.ToLowerInvariant()                             if (-not $stOldManifest.ContainsKey($sfKey)) {                                 $stChanged = $true                                 Phá vỡ                             }                             # So sánh dấu thời gian - bộ đệm ẩn có thể là DateTime hoặc chuỗi sau khi JSON roundtrip                             $cachedLWT = $stOldManifest[$sfKey]. LastWriteTimeUtc                             $fileDT = $sf. LastWriteTimeUtc                             hãy thử {                                 # Nếu bộ đệm ẩn đã là DateTime (chuyển đổi tự động ConvertFrom-Json), hãy sử dụng trực tiếp                                 if ($cachedLWT -is [DateTime]) {                                     $cachedDT = $cachedLWT.ToUniversalTime()                                 } người khác {                                     $cachedDT = [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime                                 }                                 if ([math]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) {                                     $stChanged = $true                                     Phá vỡ                                 }                             } bắt {                                 $stChanged = $true                                 Phá vỡ                             }                         }                     } người khác {                         $stChanged = $true                     }                     if (-not $stChanged) {                         # Kiểm tra xem tệp đầu ra có tồn tại không                         $stSummaryExists = Get-ChildItem (Join-Path $OutputPath "SecureBoot_Summary_*.csv") -EA SilentlyContinue | Select-Object -1 đầu tiên                         $stDashExists = Get-ChildItem (Join-Path $OutputPath "SecureBoot_Dashboard_*.html") -EA SilentlyContinue | Select-Object -1 đầu tiên                         if ($stSummaryExists -and $stDashExists) {                             Write-Host " Không phát hiện thay đổi nào ($totalFiles tệp không thay đổi) - bỏ qua xử lý" -ForegroundColor Green                             Write-Host " Bảng điều khiển cuối cùng: $($stDashExists.FullName)" -ForegroundColor White                             $cachedStats = Get-Content $stSummaryExists.Họ_Tên | ConvertFrom-Csv                             Write-Host " Devices: $($cachedStats.TotalDevices) | Đã cập nhật: $($cachedStats.Đã cập nhật) | Lỗi: $($cachedStats.WithErrors)" -ForegroundColor Gray                             Write-Host " Completed in $([math]::Round($streamSw.Elapsed.TotalSeconds, 1))s (no processing needed)" -ForegroundColor Green                             trả lại $cachedStats                         }                     } người khác {                         # DELTA PATCH: Tìm chính xác tệp nào đã thay đổi                         Write-Host " Đã phát hiện thay đổi - xác định tệp đã thay đổi..." -ForegroundColor Yellow                         $changedFiles = [System.Collections.ArrayList]::new()                         $newFiles = [System.Collections.ArrayList]::new()                         foreach ($jf in $jsonFiles) {                             $jfKey = $jf. FullName.ToLowerInvariant()                             if (-not $stOldManifest.ContainsKey($jfKey)) {                                 [void]$newFiles.Add($jf)                             } người khác {                                 $cachedLWT = $stOldManifest[$jfKey]. LastWriteTimeUtc                                 $fileDT = $jf. LastWriteTimeUtc                                 hãy thử {                                     $cachedDT = if ($cachedLWT -is [DateTime]) { $cachedLWT.ToUniversalTime() } else { [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime }                                     if ([math]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { [void]$changedFiles.Add($jf) }                                 } catch { [void]$changedFiles.Add($jf) }                             }                         }                         $totalChanged = $changedFiles.Count + $newFiles.Count                         $changePct = [toán học]::Round(($totalChanged / $totalFiles) * 100, 1)                         Write-Host " Đã thay đổi: $($changedFiles.Count) | Mới: $($newFiles.Count) | Tổng số: $totalChanged ($changePct%)" -ForegroundColor Yellow                         if ($totalChanged -gt 0 -and $changePct -lt 10) {                             # DELTA PATCH MODE: <10%, vá dữ liệu hiện có                             Write-Host " Delta patch mode ($changePct% < 10%) - patching $totalChanged files..." -ForegroundColor Green                             $dataDir = Join-Path $OutputPath "dữ liệu"                             # Tải bản ghi thiết bị đã thay đổi/mới                             $deltaDevices = @{}                             $allDeltaFiles = @($changedFiles) + @($newFiles)                             foreach ($df in $allDeltaFiles) {                                 hãy thử {                                     $devData = Get-Content $df. FullName -Raw | ConvertFrom-Json                                     $dev = Normalize-DeviceRecord $devData                                     nếu ($dev. HostName) { $deltaDevices[$dev. HostName] = $dev }                                 } bắt { }                             }                             Write-Host " Loaded $($deltaDevices.Count) changed device records" -ForegroundColor Gray                             # Đối với mỗi danh mục JSON: loại bỏ các mục cũ cho tên máy chủ đã thay đổi, thêm mục nhập mới                             $categoryFiles = @("lỗi", "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 trong $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) }                             foreach ($cat in $categoryFiles) {                                 $catPath = Join-Path $dataDir "$cat.json"                                 if (Test-Path $catPath) {                                     hãy thử {                                         $catData = Get-Content $catPath -Raw | ConvertFrom-Json                                         # Loại bỏ các mục cũ cho tên máy chủ đã thay đổi                                         $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. HostName) })                                         # Phân loại lại từng thiết bị đã thay đổi thành các danh mục                                         # (sẽ được thêm vào bên dưới sau khi phân loại)                                         $catData | ConvertTo-Json -Chiều sâu 5 | Set-Content $catPath -Mã hóa UTF8                                     } bắt { }                                 }                             }                             # Phân loại từng thiết bị đã thay đổi và gắn thêm vào các tệp danh mục phù hợp                             foreach ($dev trong $deltaDevices.Values) {                                 $slim = [ordered]@{                                     HostName = $dev. Hostname                                     WMI_Manufacturer = if ($dev. PSObject.Properties['WMI_Manufacturer']) { $dev. WMI_Manufacturer } khác { "" }                                     WMI_Model = if ($dev. PSObject.Properties['WMI_Model']) { $dev. WMI_Model } khác { "" }                                     BucketId = if ($dev. PSObject.Properties['BucketId']) { $dev. BucketId } khác { "" }                                     ConfidenceLevel = if ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } khác { "" }                                     IsUpdated = $dev. Đã cập nhật                                     UEFICA2023Error = if ($dev. PSObject.Properties['UEFICA2023Error']) { $dev. UEFICA2023Error } khác { $null }                                     SecureBootTaskStatus = if ($dev. PSObject.Properties['SecureBootTaskStatus']) { $dev. SecureBootTaskStatus } khác { "" }                                     KnownIssueId = if ($dev. PSObject.Properties['KnownIssueId']) { $dev. KnownIssueId } khác { $null }                                     SkipReasonKnownIssue = if ($dev. PSObject.Properties['SkipReasonKnownIssue']) { $dev. SkipReasonKnownIssue } else { $null }                                 }                                 $isUpd = $dev. IsUpdated -eq $true                                 $conf = if ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } khác { "" }                                 $hasErr = (-not [string]::IsNullOrEmpty($dev. UEFICA2023Error) -và $dev. UEFICA2023Error -ne "0" -và $dev. UEFICA2023Error -ne "")                                 $tskDis = ($dev. SecureBootTaskEnabled -eq $false -or $dev. SecureBootTaskStatus -eq 'Vô hiệu hóa' -hoặc $dev. SecureBootTaskStatus -eq 'NotFound')                                 $tskNF = ($dev. SecureBootTaskStatus -eq 'NotFound')                                 $sbOn = ($dev. SecureBootEnabled -ne $false -and "$($dev. SecureBootEnabled)" -ne "False")                                 $e 1801 = if ($dev. PSObject.Properties['Event1801Count']) { [int]$dev. Sự kiện1801Count } khác { 0 }                                 $e 1808 = if ($dev. PSObject.Properties['Event1808Count']) { [int]$dev. Sự kiện1808Count } khác { 0 }                                 $e 1803 = if ($dev. PSObject.Properties['Event1803Count']) { [int]$dev. Sự kiện1803Count } khác { 0 }                                 $mKEK = ($e 1803 -gt 0 -hoặc $dev. MissingKEK -eq $true)                                 $hKI = ((-not [string]::IsNullOrEmpty($dev. SkipReasonKnownIssue)) -or (-not [string]::IsNullOrEmpty($dev. KnownIssueId))                                 $rStat = if ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } khác { "" }                                 # Chắp thêm để khớp với các tệp thể loại                                 $targets = @()                                 if ($isUpd) { $targets += "updated_devices" }                                 if ($hasErr) { $targets += "lỗi" }                                 if ($hKI) { $targets += "known_issues" }                                 if ($mKEK) { $targets += "missing_kek" }                                 if (-not $isUpd -and $sbOn) { $targets += "not_updated" }                                 if ($tskDis) { $targets += "task_disabled" }                                 if (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $targets += "temp_failures" }                                 if (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -or ($tskNF -and $hasErr))) { $targets += "perm_failures" }                                 if (-not $isUpd -and (Test-ConfidenceLevel $conf 'ActionRequired')) { $targets += "action_required" }                                 if (-not $sbOn) { $targets += "secureboot_off" }                                 if ($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"                                     if (Test-Path $tgtPath) {                                         $existing = Get-Content $tgtPath -Raw | ConvertFrom-Json                                         $existing = @($existing) + @([PSCustomObject]$slim)                                         $existing | ConvertTo-Json -Chiều sâu 5 | Set-Content $tgtPath -Mã hóa UTF8                                     }                                 }                             }                             # Tái tạo CSV từ JSON được vá                             Write-Host " Regenerating CSVs from patched data..." -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"                                 if (Test-Path $catJsonPath) {                                     hãy thử {                                         $catJsonData = Get-Content $catJsonPath -Raw | ConvertFrom-Json                                         if ($catJsonData.Count -gt 0) {                                             $catJsonData | Export-Csv -Path $catCsvPath -NoTypeInformation -Encoding UTF8                                         }                                     } bắt { }                                 }                             }                             # Recount stats from the patched JSON files # Recount stats from the patched JSON files                             Write-Host " Tính toán lại tóm tắt từ dữ liệu được vá..." -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                                 if (Test-Path $catPath) { try { $cnt = (Get-Content $catPath -Raw | ConvertFrom-Json). Đếm } lần bắt { } }                                 chuyển đổi ($cat) {                                     "Updated_devices" { $pUpdated = $cnt }                                     "lỗi" { $pErrors = $cnt }                                     "Known_issues" { $pKI = $cnt }                                     "missing_kek" { $pKEK = $cnt }                                     "not_updated" { } # đã tính toán                                     "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 | ConvertFrom-Json). Đếm                             $pTotal = $pUpdated + $pNotUpdated + $pSBOff                             Write-Host " Delta patch complete: $totalChanged devices updated" -ForegroundColor Green                             Write-Host " Tổng cộng: $pTotal | Đã cập nhật: $pUpdated | NotUpdated: $pNotUpdated | Lỗi: $pErrors" -ForegroundColor White                             # Cập nhật tệp kê khai                             $stManifestDir = Join-Path $OutputPath ".cache"                             $stNewManifest = @{}                             foreach ($jf in $jsonFiles) {                                 $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{                                     LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o"); Kích cỡ = $jf. Chiều dài                                 }                             }                             Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath                             Write-Host " Completed in $([math]::Round($streamSw.Elapsed.TotalSeconds, 1))s (delta patch - $totalChanged devices)" -ForegroundColor Green                             # Đi qua để tái xử lý phát trực tuyến đầy đủ để tái tạo bảng điều khiển HTML                             # Các tệp dữ liệu đã được vá lỗi, vì vậy điều này đảm bảo bảng điều khiển luôn cập nhật                             Write-Host " Regenerating dashboard from patched data..." -ForegroundColor Yellow                         } người khác {                             Write-Host " $changePct% thay đổi (>= 10%) - yêu cầu tái xử lý phát trực tuyến đầy đủ" -ForegroundColor Yellow                         }                     }                 }             }         }         # Tạo thư mục con dữ liệu cho tệp JSON của thiết bị theo yêu cầu         $dataDir = Join-Path $OutputPath "dữ liệu"         if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null }         # K trùng lặp qua HashSet (O(1) cho mỗi tra cứu, ~50MB cho tên máy chủ 600K)         $seenHostnames = [System.Collections.Generic.HashSet[string]::new([System.StringComparer]::OrdinalIgnoreCase)         # Bộ đếm tóm tắt nhẹ (thay thế $allDevices + $uniqueDevices trong bộ nhớ)         $c = @{             Tổng cộng = 0; SBEnabled = 0; SBOff = 0             Đã cập nhật = 0; HighConf = 0; DướiObs = 0; ActionReq = 0; TempPaused = 0; NotSupported = 0; NoConfData = 0             TaskDisabled = 0; TaskNotFound = 0; TaskDisabledNotUpdated = 0             WithErrors = 0; InProgress = 0; NotyetInitiated = 0; RolloutInProgress = 0             WithKnownIssues = 0; WithMissingKEK = 0; TempFailures = 0; PermFailures = 0; NeedsReboot = 0             UpdatePending = 0         }         # Theo dõi bộ chứa cho AtRisk/SafeList (tập nhẹ)         $stFailedBuckets = [System.Collections.Generic.HashSet[string]::new()         $stSuccessBuckets = [System.Collections.Generic.HashSet[string]::new()         $stAllBuckets = @{}         $stMfrCounts = @{}         $stErrorCodeCounts = @{}; $stErrorCodeSamples = @{}         $stKnownIssueCounts = @{}         # Batch-mode tập tin dữ liệu thiết bị: tích lũy mỗi chunk, flush at chunk ranh giới         $stDeviceFiles = @("lỗi", "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         }         # Bản ghi thiết bị mỏng cho đầu ra JSON (chỉ các trường cần thiết, ~ 200 byte so với ~ 2KB đầy đủ)         hàm Get-SlimDevice {             param($Dev)             trả lại [đã đặt hàng]@{                 HostName = $Dev.HostName                 WMI_Manufacturer = if ($Dev.PSObject.Properties['WMI_Manufacturer']) { $Dev.WMI_Manufacturer } khác { "" }                 WMI_Model = if ($Dev.PSObject.Properties['WMI_Model']) { $Dev.WMI_Model } khác { "" }                 BucketId = if ($Dev.PSObject.Properties['BucketId']) { $Dev.BucketId } else { "" }                 ConfidenceLevel = if ($Dev.PSObject.Properties['ConfidenceLevel']) { $Dev.ConfidenceLevel } else { "" }                 IsUpdated = $Dev.IsUpdated                 UEFICA2023Error = if ($Dev.PSObject.Properties['UEFICA2023Error']) { $Dev.UEFICA2023Error } else { $null }                 SecureBootTaskStatus = if ($Dev.PSObject.Properties['SecureBootTaskStatus']) { $Dev.SecureBootTaskStatus } else { "" }                 KnownIssueId = if ($Dev.PSObject.Properties['KnownIssueId']) { $Dev.KnownIssueId } khác { $null }                 SkipReasonKnownIssue = if ($Dev.PSObject.Properties['SkipReasonKnownIssue']) { $Dev.SkipReasonKnownIssue } khác { $null }                 UEFICA2023Status = if ($Dev.PSObject.Properties['UEFICA2023Status']) { $Dev.UEFICA2023Status } khác { $null }                 AvailableUpdatesPolicy = if ($Dev.PSObject.Properties['AvailableUpdatesPolicy']) { $Dev.AvailableUpdatesPolicy } khác { $null }                 WinCSKeyApplied = if ($Dev.PSObject.Properties['WinCSKeyApplied']) { $Dev.WinCSKeyApplied } khác { $null }             }         }         # Flush batch to JSON file (append mode)         hàm Flush-DeviceBatch {             param([string]$StreamName, [System.Collections.Generic.List[object]]$Batch)             if ($Batch.Count -eq 0) { return }             $fPath = $stDeviceFilePaths[$StreamName]             $fSb = [System.Text.StringBuilder]::new()             foreach ($fDev in $Batch) {                 if ($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)         }         # VÒNG LẶP PHÁT TRỰC TUYẾN CHÍNH         $stChunkSize = if ($totalFiles -le 10000) { $totalFiles } khác { 10000 }         $stTotalChunks = [toán học]::Ceiling($totalFiles / $stChunkSize)         $stPeakMemMB = 0         if ($stTotalChunks -gt 1) {             Write-Host "Đang xử $totalFiles tập tin trong $stTotalChunks khối của $stChunkSize (streaming, $ParallelThreads threads):" -ForegroundColor Cyan         } người khác {             Write-Host "Processing $totalFiles files (streaming, $ParallelThreads threads):" -ForegroundColor Cyan         }         cho ($ci = 0; $ci -lt $stTotalChunks; $ci++) {             $cStart = $ci * $stChunkSize             $cEnd = [toán học]::Min($cStart + $stChunkSize, $totalFiles) - 1             $cFiles = $jsonFiles[$cStart.. $cEnd]             if ($stTotalChunks -gt 1) {                 Write-Host " Chunk $($ci + 1)/$stTotalChunks ($($cFiles.Count) tệp): " -NoNewline -ForegroundColor Gray             } người khác {                 Write-Host " Loading $($cFiles.Count) files: " -NoNewline -ForegroundColor Gray             }             $cSw = [System.Diagnostics.Stopwatch]::StartNew()             $rawDevices = Load-FilesParallel -Files $cFiles -Threads $ParallelThreads             # Danh sách lô cho mỗi khối             $cBatches = @{}             foreach ($df in $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]::new() }             $cNew = 0; $cDupe = 0             foreach ($raw in $rawDevices) {                 if (-not $raw) { continue }                 $device = Normalize-DeviceRecord $raw                 $hostname = $device. Hostname                 if (-not $hostname) { continue }                 if ($seenHostnames.Contains($hostname)) { $cDupe++; continue }                 [void]$seenHostnames.Add($hostname)                 $cNew++; $c.Total++                 $sbOn = ($device. SecureBootEnabled -ne $false -and "$($device. SecureBootEnabled)" -ne "False")                 if ($sbOn) { $c.SBEnabled++ } else { $c.SBOff++; $cBatches["secureboot_off"]. Add((Get-SlimDevice $device)) }                 $isUpd = $device. IsUpdated -eq $true                 $conf = if ($device. PSObject.Properties['ConfidenceLevel'] -and $device. ConfidenceLevel) { "$($device. ConfidenceLevel)" } khác { "" }                 $hasErr = (-not [string]::IsNullOrEmpty($device. UEFICA2023Error) -và "$($device. UEFICA2023Error)" -ne "0" -và "$($device. UEFICA2023Error)" -ne "")                 $tskDis = ($device. SecureBootTaskEnabled -eq $false -or "$($device. SecureBootTaskStatus)" -eq 'Disabled' -or "$($device. SecureBootTaskStatus)" -eq 'NotFound')                 $tskNF = ("$($device. SecureBootTaskStatus)" -eq 'NotFound')                 $bid = if ($device. PSObject.Properties['BucketId'] -and $device. BucketId) { "$($device. BucketId)" } khác { "" }                 $e 1808 = if ($device. PSObject.Properties['Event1808Count']) { [int]$device. Sự kiện1808Count } khác { 0 }                 $e 1801 = if ($device. PSObject.Properties['Event1801Count']) { [int]$device. Sự kiện1801Count } khác { 0 }                 $e 1803 = if ($device. PSObject.Properties['Event1803Count']) { [int]$device. Sự kiện1803Count } khác { 0 }                 $mKEK = ($e 1803 -gt 0 -hoặc $device. MissingKEK -eq $true -hoặc "$($device. MissingKEK)" -eq "True")                 $hKI = ((-not [string]::IsNullOrEmpty($device. SkipReasonKnownIssue)) -or (-not [string]::IsNullOrEmpty($device. KnownIssueId))                 $rStat = if ($device. PSObject.Properties['RolloutStatus']) { $device. RolloutStatus } khác { "" }                 $mfr = if ($device. PSObject.Properties['WMI_Manufacturer'] -and -not [string]::IsNullOrEmpty($device. WMI_Manufacturer)) { $device. WMI_Manufacturer } khác { "Không xác định" }                 $bid = if (-not [string]::IsNullOrEmpty($bid)) { $bid } else { "" }                 # Cờ Đang chờ Cập nhật Điện toán trước (chính sách/WinCS được áp dụng, trạng thái chưa Được cập nhật, BẬT SB, tác vụ không bị vô hiệu hóa)                 $uefiStatus = if ($device. PSObject.Properties['UEFICA2023Status']) { "$($device. UEFICA2023Status)" } else { "" }                 $hasPolicy = ($device. PSObject.Properties['AvailableUpdatesPolicy'] -and $null -ne $device. AvailableUpdatesPolicy -and "$($device. AvailableUpdatesPolicy)" -ne '')                 $hasWinCS = ($device. PSObject.Properties['WinCSKeyApplied'] -and $device. WinCSKeyApplied -eq $true)                 $statusPending = ([string]::IsNullOrEmpty($uefiStatus) -or $uefiStatus -eq 'NotStarted' -or $uefiStatus -eq 'InProgress')                 $isUpdatePending = (($hasPolicy -hoặc $hasWinCS) -and $statusPending -and -not $isUpd -and $sbOn -and -not $tskDis)                 if ($isUpd) {                     $c.Đã cập nhật++; [void]$stSuccessBuckets.Add($bid); $cBatches["updated_devices"]. Add((Get-SlimDevice $device))                     # Theo dõi các thiết bị đã cập nhật cần khởi động lại (UEFICA2023Status=Đã cập nhật nhưng Sự kiện1808=0)                     if ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) }                 }                 elseif (-not $sbOn) {                     # SecureBoot OFF — trong phạm vi, không phân loại theo sự tự tin                 }                 khác {                     if ($isUpdatePending) { } # Được tính riêng biệt trong Đang chờ xử lý Cập nhật — loại trừ lẫn nhau cho biểu đồ hình tròn                     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++ }                     else { $c.ActionReq++ }                     if ([string]::IsNullOrEmpty($conf)) { $c.NoConfData++ }                 }                 if ($tskDis) { $c.TaskDisabled++; $cBatches["task_disabled"]. Add((Get-SlimDevice $device)) }                 if ($tskNF) { $c.TaskNotFound++ }                 if (-not $isUpd -and $tskDis) { $c.TaskDisabledNotUpdated++ }                 nếu ($hasErr) {                     $c.WithErrors++; [void]$stFailedBuckets.Add($bid); $cBatches["lỗi"]. Add((Get-SlimDevice $device))                     $ec = $device. UEFICA2023Error                     if (-not $stErrorCodeCounts.ContainsKey($ec)) { $stErrorCodeCounts[$ec] = 0; $stErrorCodeSamples[$ec] = @() }                     $stErrorCodeCounts[$ec]++                     if ($stErrorCodeSamples[$ec]. Count -lt 5) { $stErrorCodeSamples[$ec] += $hostname }                 }                 if ($hKI) {                     $c.WithKnownIssues++; $cBatches["known_issues"]. Add((Get-SlimDevice $device))                     $ki = if (-not [string]::IsNullOrEmpty($device. SkipReasonKnownIssue)) { $device. SkipReasonKnownIssue } khác { $device. KnownIssueId }                     if (-not $stKnownIssueCounts.ContainsKey($ki)) { $stKnownIssueCounts[$ki] = 0 }; $stKnownIssueCounts[$ki]++                 }                 if ($mKEK) { $c.WithMissingKEK++; $cBatches["missing_kek"]. Add((Get-SlimDevice $device)) }                 if (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $c.TempFailures++; $cBatches["temp_failures"]. Add((Get-SlimDevice $device)) }                 if (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -or ($tskNF -and $hasErr))) { $c.PermFailures++; $cBatches["perm_failures"]. Add((Get-SlimDevice $device)) }                 if ($e 1801 -gt 0 và $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $c.RolloutInProgress++; $cBatches["rollout_inprogress"]. Add((Get-SlimDevice $device)) }                 if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -ne "InProgress") { $c.NotYetInitiated++ }                 if ($rStat -eq "InProgress" -and $e 1808 -eq 0) { $c.InProgress++ }                 # Đang chờ cập nhật: chính sách hoặc winCS được áp dụng, trạng thái đang chờ xử lý, SB BẬT, tác vụ không bị vô hiệu hóa                 if ($isUpdatePending) {                     $c.UpdatePending++; $cBatches["update_pending"]. Add((Get-SlimDevice $device))                 }                 if (-not $isUpd -and $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) }                 # Trong Thiết bị quan sát (tách biệt với Yêu cầu Hành động)                 if (-not $isUpd -and (Test-ConfidenceLevel $conf 'UnderObservation')) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) }                 # Hành động Bắt buộc: không cập nhật, SB BẬT, không khớp với các danh mục tin cậy khác, không cập nhật đang chờ xử lý                 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))                 }                 if (-not $stMfrCounts.ContainsKey($mfr)) { $stMfrCounts[$mfr] = @{ Total=0; Đã cập nhật=0; UpdatePending=0; HighConf=0; UnderObs=0; ActionReq=0; TempPaused=0; NotSupported=0; SBOff=0; WithErrors=0 } }                 $stMfrCounts[$mfr]. Total++                 if ($isUpd) { $stMfrCounts[$mfr]. Đã cập nhật++ }                 elseif (-not $sbOn) { $stMfrCounts[$mfr]. SBOff++ }                 elseif ($isUpdatePending) { $stMfrCounts[$mfr]. UpdatePending++ }                 elseif (Test-ConfidenceLevel $conf "HighConfidence") { $stMfrCounts[$mfr]. HighConf++ }                 elseif (Test-ConfidenceLevel $conf "UnderObservation") { $stMfrCounts[$mfr]. TrongObs++ }                 elseif (Test-ConfidenceLevel $conf "TemporarilyPaused") { $stMfrCounts[$mfr]. TempPaused++ }                 elseif (Test-ConfidenceLevel $conf "NotSupported") { $stMfrCounts[$mfr]. NotSupported++ }                 else { $stMfrCounts[$mfr]. ActionReq++ }                 if ($hasErr) { $stMfrCounts[$mfr]. WithErrors++ }                 # Theo dõi tất cả các thiết bị bằng bộ chứa (bao gồm BucketId trống)                 $bucketKey = if ($bid -and $bid -ne "") { $bid } else { "(empty)" }                 if (-not $stAllBuckets.ContainsKey($bucketKey)) {                     $stAllBuckets[$bucketKey] = @{ Count=0; Đã cập nhật=0; Manufacturer=$mfr; Model=""; BIOS="" }                     nếu ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. Mô hình = $device. WMI_Model }                     nếu ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription }                 }                 $stAllBuckets[$bucketKey]. Count++                 if ($isUpd) { $stAllBuckets[$bucketKey]. Đã cập nhật++ }             }             # Flush batches to disk             foreach ($df in $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] }             $rawDevices = $null; $cBatches = $null; [System.GC]::Collect()             $cSw.Stop()             $cTime = [Toán học]::Round($cSw.Elapsed.TotalSeconds, 1)             $cRem = $stTotalChunks - $ci - 1             $cEta = if ($cRem -gt 0) { " | ETA: ~$([Toán học]::Round($cRem * $cSw.Elapsed.TotalSeconds / 60, 1)) min" } else { "" }             $cMem = [math]::Round([System.GC]::GetTotalMemory($false) / 1MB, 0)             if ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem }             Write-Host " +$cNew, giá $cDupe, ${cTime}s | Mem: ${cMem}MB$cEta" -ForegroundColor Green         }         # Hoàn thiện mảng JSON         foreach ($dfName in $stDeviceFiles) {             [System.IO.File]::AppendAllText($stDeviceFilePaths[$dfName], "'n]", [System.Text.Encoding]::UTF8)             Write-Host " $dfName.json: $($stDeviceFileCounts[$dfName]) devices" -ForegroundColor DarkGray         }         # Tính toán thống kê dẫn xuất         $stAtRisk = 0; $stSafeList = 0         foreach ($bid trong $stAllBuckets.Keys) {             $b = $stAllBuckets[$bid]; $nu = $b.Count - Cập nhật $b.Đã cập nhật             if ($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ập nhật = $c.Đã cập nhật; 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.Đã cập nhật             UpdatesPending = $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; UpdatePending = $c.UpdatePending             AtRiskDevices = $stAtRisk; SafeListDevices = $stSafeList             PercentWithErrors = if ($c.Total -gt 0) { [math]::Round($c.WithErrors/$c.Total)*100,2) } else { 0 }             PercentAtRisk = if ($c.Total -gt 0) { [math]::Round(($stAtRisk/$c.Total)*100,2) } else { 0 }             PercentSafeList = if ($c.Total -gt 0) { [math]::Round(($stSafeList/$c.Total)*100,2) } else { 0 }             PercentHighConfidence = if ($c.Total -gt 0) { [math]::Round(($c.HighConf/$c.Total)*100,1) } khác { 0 }             PercentCertUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 }             PercentActionBắt buộc = if ($c.Total -gt 0) { [math]::Round($c.ActionReq/$c.Total)*100,1) } khác { 0 }             PercentNotUptodate = if ($c.Total -gt 0) { [math]::Round($stNotUptodate/$c.Total*100,1) } else { 0 }             PercentFullyUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 }             UniqueBuckets = $stAllBuckets.Count; PeakMemoryMB = $stPeakMemMB; ProcessingMode = "Phát trực tuyến"         }         # Viết CSVs         [PSCustomObject]$stats | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_Summary_$timestamp.csv") -NoTypeInformation -Encoding UTF8         $stMfrCounts.GetEnumerator() | Sort-Object với { $_. Value.Total } -Giảm dần | ForEach-Object {             [PSCustomObject]@{ Manufacturer=$_. Phím; Count=$_. Value.Total; Đã cập nhật=$_. Value.Updated; HighConfidence=$_. Value.HighConf; ActionRequired=$_. Value.ActionReq }         } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_ByManufacturer_$timestamp.csv") -NoTypeInformation -Encoding UTF8         $stErrorCodeCounts.GetEnumerator() | Sort-Object -Giảm dần | ForEach-Object {             [PSCustomObject]@{ ErrorCode=$_. Phím; Count=$_. Giá trị; SampleDevices=($stErrorCodeSamples[$_. Khóa] -tham gia ", ") }         } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_ErrorCodes_$timestamp.csv") -NoTypeInformation -Encoding UTF8         $stAllBuckets.GetEnumerator() | Sort-Object { $_. Value.Count } -Giảm dần | ForEach-Object {             [PSCustomObject]@{ BucketId=$_. Phím; Count=$_. Value.Count; Đã cập nhật=$_. Value.Updated; NotUpdated=$_. Value.Count-$_. Value.Updated; Nhà sản xuất=$_. Value.Manufacturer }         } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_UniqueBuckets_$timestamp.csv") -NoTypeInformation -Encoding UTF8         # Tạo CSV tương thích với dàn nhạc (tên tệp dự kiến cho người Start-SecureBootRolloutOrchestrator.ps1)         $notUpdatedJsonPath = Join-Path $dataDir "not_updated.json"         if (Test-Path $notUpdatedJsonPath) {             hãy thử {                 $nuData = Get-Content $notUpdatedJsonPath -Raw | ConvertFrom-Json                 if ($nuData.Count -gt 0) {                     # NotUptodate CSV - orchestrator tìm kiếm *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) devices)" -ForegroundColor Gray                 }             } bắt { }         }         # Viết dữ liệu JSON cho bảng điều khiển         $stats | ConvertTo-Json -Chiều sâu 3 | Set-Content (Join-Path $dataDir "summary.json") -Encoding UTF8         # THEO DÕI LỊCH SỬ: Lưu điểm dữ liệu cho biểu đồ xu hướng         # Sử dụng vị trí bộ đệm ẩn ổn định để dữ liệu xu hướng duy trì trên các thư mục tổng hợp có dấu thời gian.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   # Nếu OutputPath trông giống như "...\Aggregation_yyyyMMdd_HHmmss", bộ đệm ẩn sẽ đi vào thư mục chính.# Nếu không, bộ đệm ẩn sẽ nằm bên trong OutputPath.$parentDir = Split-Path $OutputPath -Parent         $leafName = Split-Path $OutputPath -Leaf         if ($leafName -match '^Aggregation_\d{8}' -or $leafName -eq 'Aggregation_Current') {             # Thư mục dấu thời gian do Orchestrator tạo — sử dụng bộ đệm ẩn ổn định             $historyPath = Join-Path $parentDir ".cache\trend_history.json"         } người khác {             $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 = @()         if (Test-Path $historyPath) {             hãy thử { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } catch { $historyData = @() }         }         # Ngoài ra, hãy kiểm tra bên trong OutputPath\.cache\ (vị trí thừa tự từ các phiên bản cũ hơn)         # Phối bất kỳ điểm dữ liệu nào chưa có trong lịch sử chính         if ($leafName -eq 'Aggregation_Current' -hoặc $leafName -match '^Aggregation_\d{8}') {             $innerHistoryPath = Join-Path $OutputPath ".cache\trend_history.json"             if ((Test-Path $innerHistoryPath) -and $innerHistoryPath -ne $historyPath) {                 hãy thử {                     $innerData = @(Get-Content $innerHistoryPath -Raw | ConvertFrom-Json)                     $existingDates = @($historyData | ForEach-Object { $_. Ngày })                     foreach ($entry in $innerData) {                         nếu ($entry. Ngày -và giờ $entry. Ngày -notin $existingDates) {                             $historyData += $entry                         }                     }                     if ($innerData.Count -gt 0) {                         Write-Host " Merged $($innerData.Count) data points from inner cache" -ForegroundColor DarkGray                     }                 } bắt { }             }         }

        # BOOTSTRAP: Nếu lịch sử xu hướng trống/êm, hãy tái cấu trúc từ dữ liệu lịch sử         if ($historyData.Count -lt 2 -and ($leafName -match '^Aggregation_\d{8}' -or $leafName -eq 'Aggregation_Current')) {             Write-Host " Lịch sử xu hướng khởi động từ dữ liệu lịch sử..." -ForegroundColor Yellow             $dailyData = @{}                          # Nguồn 1: CSV tóm tắt bên trong thư mục hiện tại (Aggregation_Current tất cả các CSV Tóm tắt)             $localSummaries = Get-ChildItem $OutputPath -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Sort-Object tên             foreach ($summCsv in $localSummaries) {                 hãy thử {                     $summ = Import-Csv $summCsv.Họ_Tên | Select-Object -1 đầu tiên                     nếu ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0 -và $summ. ReportGeneratedAt) {                         $dateStr = ([datetime]$summ. ReportGeneratedAt). ToString("yyyy-MM-dd")                         $updated = if ($summ. Đã cập nhật) { [int]$summ. Đã cập nhật } khác { 0 }                         $notUpd = if ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated }                         $dailyData[$dateStr] = [PSCustomObject]@{                             Ngày = $dateStr; Tổng = [int]$summ. TotalDevices; Đã cập nhật = $updated; NotUpdated = $notUpd                             NeedsReboot = 0; Lỗi = 0; ActionBắt buộc = if ($summ. ActionRequired) { [int]$summ. Hành độngBắt buộc } khác { 0 }                         }                     }                 } bắt { }             }                          # Nguồn 2: Cặp dấu thời gian Aggregation_* cũ (thừa tự, nếu chúng vẫn tồn tại)             $aggFolders = Get-ChildItem $parentDir -Directory -Filter "Aggregation_*" -EA SilentlyContinue |                 Where-Object { $_. Tên -match '^Aggregation_\d{8}' } |                 Sort-Object tên             foreach ($folder in $aggFolders) {                 $summCsv = Get-ChildItem $folder. FullName -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Select-Object -1 đầu tiên                 nếu ($summCsv) {                     hãy thử {                         $summ = Import-Csv $summCsv.Họ_Tên | Select-Object -1 đầu tiên                         nếu ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0) {                             $dateStr = $folder. Name -replace '^Aggregation_(\d{4})(\d{2})(\d{2})_.*', '$1-$2-$3'                             $updated = if ($summ. Đã cập nhật) { [int]$summ. Đã cập nhật } khác { 0 }                             $notUpd = if ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated }                             $dailyData[$dateStr] = [PSCustomObject]@{                                 Ngày = $dateStr; Tổng = [int]$summ. TotalDevices; Đã cập nhật = $updated; NotUpdated = $notUpd                                 NeedsReboot = 0; Lỗi = 0; ActionBắt buộc = if ($summ. ActionRequired) { [int]$summ. Hành độngBắt buộc } khác { 0 }                             }                         }                     } bắt { }                 }             }                          # Nguồn 3: RolloutState.json WaveHistory (có dấu thời gian cho mỗi đợt từ ngày 1)             # Điều này cung cấp các điểm dữ liệu đường cơ sở ngay cả khi không tồn tại thư mục tổng hợp cũ             $rolloutStatePaths = @(                 (Đường dẫn Tham gia $parentDir "RolloutState\RolloutState.json"),                 (Đường dẫn Tham gia $OutputPath "RolloutState\RolloutState.json")             )             foreach ($rsPath in $rolloutStatePaths) {                 if (Test-Path $rsPath) {                     hãy thử {                         $rsData = Get-Content $rsPath -Raw | ConvertFrom-Json                         if ($rsData.WaveHistory) {                             # Sử dụng ngày bắt đầu sóng làm điểm dữ liệu xu hướng                             # Tính toán các thiết bị tích lũy nhắm vào mỗi đợt                             $cumulativeTargeted = 0                             foreach ($wave trong $rsData.WaveHistory) {                                 nếu ($wave. StartedAt -and $wave. DeviceCount) {                                     $waveDate = ([datetime]$wave. Đã bắt đầuTa). ToString("yyyy-MM-dd")                                     $cumulativeTargeted += [int]$wave. DeviceCount                                     if (-not $dailyData.ContainsKey($waveDate)) {                                         # Xấp xỉ: vào thời điểm bắt đầu sóng, chỉ các thiết bị từ các sóng trước đó mới được cập nhật                                         $dailyData[$waveDate] = [PSCustomObject]@{                                             Ngày = $waveDate; Tổng = $c.Tổng; Đã cập nhật = [toán học]::Max(0, $cumulativeTargeted - [int]$wave. DeviceCount)                                             NotUpdated = $c.Total - [math]::Max(0, $cumulativeTargeted - [int]$wave. DeviceCount)                                             NeedsReboot = 0; Lỗi = 0; Hành độngBắt buộc = 0                                         }                                     }                                 }                             }                         }                     } bắt { }                     break # Use first found                 }             }

            if ($dailyData.Count -gt 0) {                 $historyData = @($dailyData.GetEnumerator() | Sort-Object | ForEach-Object { $_. Giá trị })                 Write-Host " Bootstrapped $($historyData.Count) data points from historical summaries" -ForegroundColor Green             }         }

        # Thêm điểm dữ liệu hiện tại (deduplicate by day - keep latest mỗi ngày)         $todayKey = (Get-Date). ToString("yyyy-MM-dd")         $existingToday = $historyData | Where-Object { "$($_. Date)" -like "$todayKey*" }         if ($existingToday) {             # Thay thế mục nhập hôm nay             $historyData = @($historyData | Where-Object { "$($_. Date)" -notlike "$todayKey*" })         }         $historyData += [PSCustomObject]@{             Ngày = $todayKey             Tổng = $c.Tổng             Đã cập nhật = $c.Đã cập nhật             NotUpdated = $stNotUptodate             NeedsReboot = $c.NeedsReboot             Lỗi = $c.WithErrors             ActionRequired = $c.ActionReq         }         # Loại bỏ điểm dữ liệu không hợp lệ (tổng cộng 0) và giữ 90 điểm cuối cùng         $historyData = @($historyData | Where-Object { [int]$_. Tổng cộng -gt 0 })         # Không có giới hạn — dữ liệu xu hướng là ~100 byte/mục nhập, một năm đầy đủ = ~36 KB         $historyData | ConvertTo-Json -Chiều sâu 3 | Set-Content $historyPath -Mã hóa UTF8         Write-Host " Lịch sử xu hướng: $($historyData.Count) điểm dữ liệu" -ForegroundColor DarkGray                  # Xây dựng dữ liệu biểu đồ xu hướng cho HTML         $trendLabels = ($historyData | ForEach-Object { "'$($_. Date)'" }) -join ","         $trendUpdated = ($historyData | ForEach-Object { $_. Đã cập nhật }) -join ","         $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) -join ","         $trendTotal = ($historyData | ForEach-Object { $_. Total }) -tham gia ","         # Chiếu: mở rộng đường xu hướng bằng cách tăng gấp đôi hàm mũ (2,4,8,16...)         # Lấy kích thước sóng và thời gian quan sát từ dữ liệu lịch sử xu hướng thực tế.# - Kích thước sóng = tăng một lần lớn nhất trong lịch sử (đợt gần đây nhất được triển khai)         # - Ngày quan sát = ngày lịch trung bình giữa các điểm dữ liệu xu hướng (tần suất chúng tôi chạy)         # Sau đó tăng gấp đôi kích thước sóng mỗi giai đoạn, phù hợp với chiến lược tăng trưởng 2x của orchestrator.$projLabels = ""; $projUpdated = ""; $projNotUpdated = ""; $hasProjection = $false         if ($historyData.Count -ge 2) {             $lastUpdated = $c.Đã cập nhật             $remaining = $stNotUptodate # Chỉ thiết bị không cập nhật SB-ON (ngoại trừ SecureBoot OFF)             $projDates = @(); $projValues = @(); $projNotUpdValues = @()             $projDate = Get-Date

            # Kích thước sóng và thời gian quan sát lấy từ lịch sử xu hướng             $increments = @()             $dayGaps = @()             cho ($hi = 1; $hi -lt $historyData.Count; $hi++) {                 $inc = $historyData[$hi]. Đã cập nhật - $historyData[$hi-1]. Cập nhật                 if ($inc -gt 0) { $increments += $inc }                 hãy thử {                     $d 1 = [datetime]::P arse($historyData[$hi-1]. Ngày)                     $d 2 = [datetime]::P arse($historyData[$hi]. Ngày)                     $gap = ($d 2 - $d 1). Tổng số Ngày                     if ($gap -gt 0) { $dayGaps += $gap }                 } bắt {}             }             # Kích thước sóng = gần đây nhất tích cực gia tăng (sóng hiện tại), dự phòng để trung bình, tối thiểu 2             $waveSize = if ($increments. Đếm -gt 0) {                 [toán học]::Max(2, $increments[-1])             } khác { 2 }             # Kỳ quan sát = khoảng cách trung bình giữa các điểm dữ liệu (ngày theo lịch trên sóng), tối thiểu 1             $waveDays = if ($dayGaps.Count -gt 0) {                 [toán học]::Max(1, [toán học]::Round(($dayGaps | Measure-Object -Average). Trung bình, 0))             } khác { 1 }

            Write-Host " Chiếu: waveSize=$waveSize (từ gia số cuối cùng), waveDays=$waveDays (khoảng cách trung bình từ lịch sử)" -ForegroundColor DarkGray

            $dayCounter = 0             # Chiếu cho đến khi tất cả các thiết bị được cập nhật hoặc tối đa 365 ngày             cho ($pi = 1; $pi -le 365; $pi++) {                 $projDate = $projDate.AddDays(1)                 $dayCounter++                 # Tại mỗi ranh giới của kỳ quan sát, triển khai một sóng rồi tăng gấp đôi                 if ($dayCounter -ge $waveDays) {                     $devicesThisWave = [math]::Min($waveSize, $remaining)                     $lastUpdated += $devicesThisWave                     $remaining -= $devicesThisWave                     if ($lastUpdated -gt ($c.Đã cập nhật + $stNotUptodate)) { $lastUpdated = $c.Cập nhật + $stNotUptodate; $remaining = 0 }                     # Kích thước sóng đôi cho giai đoạn tiếp theo (orchestrator chiến lược 2x)                     $waveSize = $waveSize * 2                     $dayCounter = 0                 }                 $projDates += "'$($projDate.ToString("yyyy-MM-dd"))'"                 $projValues += $lastUpdated                 $projNotUpdValues += [toán học]::Max(0, $remaining)                 if ($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 " Projection: need at least 2 trend data points to derive wave timing" -ForegroundColor DarkGray         }         # Xây dựng chuỗi dữ liệu biểu đồ kết hợp cho chuỗi tại đây         $allChartLabels = if ($hasProjection) { "$trendLabels,$projLabels" } khác { $trendLabels }         $projDataJS = if ($hasProjection) { $projUpdated } khác { "" }         $projNotUpdJS = if ($hasProjection) { $projNotUpdated } khác { "" }         $histCount = ($historyData | Thước đo-Đối tượng). Đếm         $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Giảm dần | ForEach-Object {             @{ name=$_. Phím; total=$_. Value.Total; đã cập nhật=$_. Value.Updated; highConf=$_. Value.HighConf; actionReq=$_. Value.ActionReq }         } | ConvertTo-Json -Chiều sâu 3 | Set-Content (Join-Path $dataDir "manufacturers.json") -Mã hóa UTF8         # Chuyển đổi tệp dữ liệu JSON thành CSV cho nội dung tải xuống Excel mà con người có thể đọc được         Write-Host "Chuyển đổi dữ liệu thiết bị thành CSV cho Excel tải xuống..." -ForegroundColor Gray         foreach ($dfName in $stDeviceFiles) {             $jsonFile = Join-Path $dataDir "$dfName.json"             $csvFile = Join-Path $OutputPath "SecureBoot_${dfName}_$timestamp.csv"             if (Test-Path $jsonFile) {                 hãy thử {                     $jsonData = Get-Content $jsonFile -Raw | ConvertFrom-Json                     if ($jsonData.Count -gt 0) {                         # Bao gồm các cột bổ sung update_pending CSV                         $selectProps = if ($dfName -eq "update_pending") {                             @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Status', 'UEFICA2023Error', 'AvailableUpdatesPolicy', 'WinCSKeyApplied', 'SecureBootTaskStatus')                         } người khác {                             @('HostName', '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) hàng -> CSV" -ForegroundColor DarkGray                     }                 } catch { Write-Host " $dfName - skipped" -ForegroundColor DarkYellow }             }         }         # Tạo bảng điều khiển HTML tự chứa         $htmlPath = Join-Path $OutputPath "SecureBoot_Dashboard_$timestamp.html"         Write-Host "Generating self-contained HTML dashboard..." -ForegroundColor Yellow         # VELOCITY PROJECTION: Tính toán từ lịch sử quét hoặc tóm tắt trước đó         $stDeadline = [datetime]"2026-06-24" # Chứng chỉ KEK hết hạn         $stDaysToDeadline = [toán học]::Max(0, ($stDeadline - (Get-Date)). Ngày)         $stDevicesPerDay = 0         $stProjectedDate = $null         $stVelocitySource = "Không áp dụng"         $stWorkingDays = 0         $stCalendarDays = 0         # Trước tiên hãy thử lịch sử xu hướng (trọng lượng nhẹ, đã được duy trì bởi bộ tập hợp — thay thế ScanHistory.json)         if ($historyData.Count -ge 2) {             $validHistory = @($historyData | Where-Object { [int]$_. Tổng -gt 0 -và [int]$_. Đã cập nhật -ge 0 })             if ($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 = [datetime]::P arse($curr. Date.Substring(0, [Math]::Min(10, $curr. Date.Length)))                 $daysDiff = ($currDate - $prevDate). Tổng số Ngày                 if ($daysDiff -gt 0) {                     $updDiff = [int]$curr. Đã cập nhật - [int]$prev. Cập nhật                     nếu ($updDiff -gt 0) {                         $stDevicesPerDay = [toán học]::Round($updDiff / $daysDiff, 0)                         $stVelocitySource = "TrendHistory"                     }                 }             }         }         # Thử tóm tắt triển khai orchestrator (có tốc độ tính toán trước)         if ($stVelocitySource -eq "N/A" -and $RolloutSummaryPath -and (Test-Path $RolloutSummaryPath)) {             hãy thử {                 $rolloutSummary = Get-Content $RolloutSummaryPath -Raw | ConvertFrom-Json                 if ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) {                     $stDevicesPerDay = [math]::Round([double]$rolloutSummary.DevicesPerDay, 1)                     $stVelocitySource = "Orchestrator"                     if ($rolloutSummary.ProjectedCompletionDate) {                         $stProjectedDate = $rolloutSummary.ProjectedCompletionDate                     }                     if ($rolloutSummary.WorkingDaysRemaining) { $stWorkingDays = [int]$rolloutSummary.WorkingDaysRemaining }                     if ($rolloutSummary.CalendarDaysRemaining) { $stCalendarDays = [int]$rolloutSummary.CalendarDaysRemaining }                 }             } bắt { }         }         # Bản dự phòng: thử CSV tóm tắt trước đó (tìm kiếm trong thư mục hiện tại VÀ thư mục tổng hợp cha mẹ/anh chị em)         if ($stVelocitySource -eq "N/A") {             $searchPaths = @(                 (Đường dẫn Tham gia $OutputPath "SecureBoot_Summary_*.csv")             )             # Ngoài ra, tìm kiếm các thư mục tổng hợp anh em (orchestrator tạo thư mục mới mỗi lần chạy)             $parentPath = Split-Path $OutputPath -Parent             if ($parentPath) {                 $searchPaths += (Đường dẫn Tham gia $parentPath "Aggregation_*\SecureBoot_Summary_*.csv")                 $searchPaths += (Đường dẫn Tham gia $parentPath "SecureBoot_Summary_*.csv")             }             $prevSummary = $searchPaths | ForEach-Object { Get-ChildItem $_ -EA SilentlyContinue } | Sort-Object LastWriteTime -Giảm dần | Select-Object -1 đầu tiên             if ($prevSummary) {                 hãy thử {                     $prevStats = Get-Content $prevSummary.Họ_Tên | ConvertFrom-Csv                     $prevDate = [datetime]$prevStats.ReportGeneratedAt                     $daysSinceLast = ((Get-Date) - $prevDate). Tổng số Ngày                     nếu ($daysSinceLast -gt 0,01) {                         $prevUpdated = [int]$prevStats.Updated                         $updDelta = $c.Cập nhật - Bản cập $prevUpdated                         nếu ($updDelta -gt 0) {                             $stDevicesPerDay = [math]::Round($updDelta / $daysSinceLast, 0)                             $stVelocitySource = "Báo cáo Trước"                         }                     }                 } bắt { }             }         }         # Dự phòng: tính vận tốc từ span lịch sử xu hướng đầy đủ (đầu tiên so với điểm dữ liệu mới nhất)         if ($stVelocitySource -eq "N/A" -and $historyData.Count -ge 2) {             $validHistory = @($historyData | Where-Object { [int]$_. Tổng -gt 0 -và [int]$_. Đã cập nhật -ge 0 })             if ($validHistory.Count -ge 2) {                 $first = $validHistory[0]                 $last = $validHistory[-1]                 $firstDate = [datetime]::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). Tổng số Ngày                 if ($daysDiff -gt 0) {                     $updDiff = [int]$last. Đã cập nhật - [int]$first. Cập nhật                     if ($updDiff -gt 0) {                         $stDevicesPerDay = [toán học]::Round($updDiff / $daysDiff, 1)                         $stVelocitySource = "TrendHistory"                     }                 }             }         }         # Tính toán chiếu bằng cách dùng phép nhân hàm mũ (nhất quán với biểu đồ xu hướng)         # Tái sử dụng dữ liệu chiếu đã được tính cho biểu đồ nếu có         if ($hasProjection -and $projDates.Count -gt 0) {             # Sử dụng ngày dự kiến gần nhất (khi tất cả các thiết bị được cập nhật)             $lastProjDateStr = $projDates[-1] -replace "'", ""             $stProjectedDate = ([datetime]::P arse($lastProjDateStr)). ToString("MMM dd, yyyy")             $stCalendarDays = ([datetime]::P arse($lastProjDateStr) - (Get-Date)). Ngày             $stWorkingDays = 0             $d = Get-Date             cho ($i = 0; $i -lt $stCalendarDays; $i++) {                 $d = $d.AddDays(1)                 if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ }             }         } elseif ($stDevicesPerDay -gt 0 -và $stNotUptodate -gt 0) {             # Dự phòng: chiếu tuyến tính nếu không có dữ liệu hàm mũ             $daysNeeded = [math]::Ceiling($stNotUptodate / $stDevicesPerDay)             $stProjectedDate = (Get-Date). AddDays($daysNeeded). ToString("MMM dd, yyyy")             $stWorkingDays = 0; $stCalendarDays = $daysNeeded             $d = Get-Date             cho ($i = 0; $i -lt $daysNeeded; $i++) {                 $d = $d.AddDays(1)                 if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ }             }         }         # Build velocity HTML         $velocityHtml = if ($stDevicesPerDay -gt 0) {             "<div><mạnh>&#128640; Thiết bị/Ngày:</strong> $($stDevicesPerDay.ToString('N0')) (source: $stVelocitySource)</div>" +             "<div><strong>&#128197; Hoàn thành Dự kiến:</strong> $stProjectedDate" +             $(if ($stProjectedDate -and [datetime]::P arse($stProjectedDate) -gt $stDeadline) { " <span style='color:#dc3545; font-weight:bold'>&#9888; PAST DEADLINE</span>" } else { " <span style='color:#28a745'>&#10003; Before deadline</span>" }) +             "</div>" +             "<div><strong>&#128336; Ngày làm việc:</strong> $stWorkingDays | <ngày>Calendar mạnh:</strong> $stCalendarDays</div>" +             "<div style='font-size:.8em; color:#888'>Hạn chót: Ngày 24 tháng 6 năm 2026 (Hết hạn chứng chỉ KEK) | Ngày còn lại: $stDaysToDeadline</div>"         } người khác {             "<div style='padding:8px; nền:#fff3cd; biên giới bán kính:4px; viền trái:3px rắn #ffc107'>" +             "<các>&#128197; Hoàn thành Dự kiến:</strong> không đủ dữ liệu để tính toán vận tốc.                                                                                  " +             "Chạy tổng hợp ít nhất hai lần với các thay đổi dữ liệu để thiết lập rate.<br/>" +             "<hạn>hạn </strong> 24/6/2026 (Hết hạn chứng chỉ KEK) | <ngày>còn lại:</strong> $stDaysToDeadline</div>"         }                  # Đếm ngược hết hạn chứng chỉ         $certToday = Get-Date         $certKekExpiry = [datetime]"2026-06-24"         $certUefiExpiry = [datetime]"2026-06-27"         $certPcaExpiry = [datetime]"2026-10-19"         $daysToKek = [toán học]::Max(0, (số $certKekExpiry - $certToday). Ngày)         $daysToUefi = [toán học]::Max(0, (số $certUefiExpiry - $certToday). Ngày)         $daysToPca = [toán học]::Max(0, (số $certPcaExpiry - $certToday). Ngày)         $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } khác { '#28a745' }                  # Người trợ giúp: Đọc bản ghi từ JSON, xây dựng tóm tắt bộ chứa + hàng N đầu tiên trên thiết bị         $maxInlineRows = 200         hàm Build-InlineTable {             param([string]$JsonPath, [int]$MaxRows = 200, [string]$CsvFileName = "")             $bucketSummary = ""             $deviceRows = ""             $totalCount = 0             if (Test-Path $JsonPath) {                 hãy thử {                     $data = Get-Content $JsonPath -Raw | ConvertFrom-Json                     $totalCount = $data. Đếm                                          # BUCKET SUMMARY: Nhóm theo BucketId, hiển thị số lượng trên mỗi bộ chứa với Cập nhật từ thống kê bộ chứa toàn cầu                     if ($totalCount -gt 0) {                         $buckets = $data | Group-Object BucketId | Sort-Object số -Giảm dần                         $bucketSummary = "><2 h3 style='font-size:,95em; màu:#333; margin:10px 0 5px'><3 By Hardware Bucket ($($buckets. Count) buckets)><4 /h3>"                         $bucketSummary += "><6 div style='max-height:300px; tràn-y:auto; margin-bottom:15px'><table><thead><tr><th><5 BucketID><6 /><th th style='text-align:right'>Total</th><th style='text-align:right; color:#28a745'>cập</th><th style='text-align:right; color:#dc3545'>Not Updated</th><th><1 Manufacturer><2 /th></tr></thead><tbody>"                         foreach ($b in $buckets) {                             $bid = if ($b.Name) { $b.Name } else { "(empty)" }                             $mfr = ($b.Nhóm | Select-Object -1 đầu tiên). WMI_Manufacturer                             # Nhận số lượng Cập nhật từ thống kê bộ chứa toàn cầu (tất cả các thiết bị trong bộ chứa này trên toàn bộ tập dữ liệu)                             $lookupKey = $bid                             $globalBucket = if ($stAllBuckets.ContainsKey($lookupKey)) { $stAllBuckets[$lookupKey] } khác { $null }                             $bUpdatedGlobal = if ($globalBucket) { $globalBucket.Updated } khác { 0 }                             $bTotalGlobal = if ($globalBucket) { $globalBucket.Count } khác { $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; màu:#28a745; font-weight:bold'>$bUpdatedGlobal><2 /td><td style='text-align:right; màu:#dc3545; font-weight:bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n"                         }                         $bucketSummary += "</tbody></table></div>"                     }                                          # CHI TIẾT THIẾT BỊ: Hàng N đầu tiên dưới dạng danh sách phẳng                     $slice = $data | Select-Object -First $MaxRows                     foreach ($d in $slice) {                         $conf = $d.ConfidenceLevel                         $confBadge = if ($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 "Paused") { '<span class="badge-warning">Paused><4 /span>' }                                      else { '<span class="badge-warning">Action Req><8 /span>' }                         $statusBadge = if ($d.IsUpdated) { '><00 span class="badge-success"><01 Updated</span>' }                                        elseif ($d.UEFICA2023Error) { '><04 span class="badge-danger"><05 Error</span>' }                                        else { '><08 span class="badge-warning"><09 Pending><0 /span>' }                         $deviceRows += "><12 tr><td><5 $($d.HostName)><16 /td><td><9 $($d.WMI_Manufacturer)$d ><20 /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 /td><td style='font-size:.75em'><39 $($d.BucketId)><40 /td></tr><3 'n"                     }                 } bắt { }             }             if ($totalCount -eq 0) {                 trả về "><44 div style='padding:20px; màu:#888; font-style:italic'><45 No devices in this category.><46 /div>"             }             $showing = [toán học]::Min($MaxRows, $totalCount)             $header = "><48 div style='margin:5px 0; cỡ phông:,85em; color:#666'><49 Total: $($totalCount.ToString("N0")) devices"             if ($CsvFileName) { $header += " | ><50 a href='$CsvFileName' style='color:#1a237e; font-weight:bold'>&#128196; Tải xuống CSV đầy đủ cho Excel><3 /a>" }             $header += "><55 /div>"             $deviceHeader = "><57 h3 style='font-size:,95em; màu:#333; margin:10px 0 5px'><58 Device Details (hiển thị thông tin $showing)><59 /h3>"             $deviceTable = "><61 div style='max-height:500px; overflow-y:auto'><table><thead><tr><th><0 HostName><1 /th><th><4 Manufacturer><5 /th><th><8 Model><9 /th><th><2 Confidence><3 /th th><th><><6><7 /th><th><0 Lỗi><1 /th><><4 BucketId><5 /th></tr></thead><tbody><2 $deviceRows><3 /tbody></table></div>"             return "$header$bucketSummary$deviceHeader$deviceTable"         }                  # Xây dựng bảng nội tuyến từ các tệp JSON đã có trên đĩa, liên kết đến CSV         $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"         # Bảng tùy chỉnh để Chờ Cập nhật — bao gồm cột UEFICA2023Status và UEFICA2023Error         $tblUpdatePending = ""         $upJsonPath = Join-Path $dataDir "update_pending.json"         if (Test-Path $upJsonPath) {             hãy thử {                 $upData = Get-Content $upJsonPath -Raw | ConvertFrom-Json                 $upCount = $upData.Count                 if ($upCount -gt 0) {                     $upHeader = "<div style='margin:5px 0; cỡ phông:,85em; color:#666'>Total: $($upCount.ToString("N0")) devices | <a href='SecureBoot_update_pending_$timestamp.csv' style='color:#1a237e; font-weight:bold'>&#128196; Tải xuống CSV đầy đủ cho Excel><4 /a></div>"                     $upRows = ""                     $upSlice = $upData | Select-Object -First $maxInlineRows                     foreach ($d in $upSlice) {                         $uefiSt = if ($d.UEFICA2023Status) { $d.UEFICA2023Status } else { '<span style="color:#999">null><0 /span>' }                         $uefiErr = if ($d.UEFICA2023Error) { "<span style='color:#dc3545'>$($d.UEFICA2023Error)</span>" } khác { '-' }                         $policyVal = if ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } else { '-' }                         $wincsVal = if ($d.WinCSKeyApplied) { '<span class="badge-success">Yes><8 /span>' } else { '-' }                         $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; màu:#333; margin:10px 0 5px'>Device Details (hiển thị thông tin $upShowing đầu tiên)</h3>"                     $upTable = "<div style='max-height:500px; overflow-y:auto'><table><thead><tr><th><9 HostName><0 /th><th><3 Manufacturer><4 /th><th><7 Model><8 /th><th><1 UEFICA2023Status><2 /th><th><5 UEFICA2023Ror><6 /th><th><9 Policy</th><th>th WinCS Key</th><th>BucketId</th></tr></thead><tbody><5 $upRows><6 /tbody></table></div>"                     $tblUpdatePending = "$upHeader$upDevHeader$upTable"                 } người khác {                     $tblUpdatePending = "<div style='padding:20px; màu:#888; font-style:italic'>No devices in this category.</div>"                 }             } bắt {                 $tblUpdatePending = "<div style='padding:20px; màu:#888; font-style:italic'>No devices in this category.</div>"             }         } người khác {             $tblUpdatePending = "<div style='padding:20px; màu:#888; font-style:italic'>No devices in this category.</div>"         }                  # Đếm ngược hết hạn chứng chỉ         $certToday = Get-Date         $certKekExpiry = [datetime]"2026-06-24"         $certUefiExpiry = [datetime]"2026-06-27"         $certPcaExpiry = [datetime]"2026-10-19"         $daysToKek = [toán học]::Max(0, (số $certKekExpiry - $certToday). Ngày)         $daysToUefi = [toán học]::Max(0, (số $certUefiExpiry - $certToday). Ngày)         $daysToPca = [toán học]::Max(0, (số $certPcaExpiry - $certToday). Ngày)         $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } khác { '#28a745' }                  # Xây dựng biểu đồ nhà sản xuất dữ liệu tại chỗ (10 vị trí hàng đầu theo số lượng thiết bị)         $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Giảm dần | Select-Object -10 đầu tiên         $mfrChartTitle = if ($stMfrCounts.Count -le 10) { "By Manufacturer" } else { "Top 10 Manufacturers" }         $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_. Khóa)'" }) -tham gia ","         $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 ","                  # Xây dựng bảng nhà sản xuất         $mfrTableRows = ""         $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Giảm dần | ForEach-Object {             $mfrTableRows += "<tr><td><7 $($_. Phím)</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 close-tag fragments: chia thành các phần để nền tảng CMS web không         # coi chúng là HTML thực và đưa các ký tự Unicode vô hình xung quanh chúng.$endScript = '</scr' + 'ipt>'         $endStyle = '</sty' + 'le>'         $endHead = '</he' + 'ad>'         $endBody = '</bo' + 'dy>'         $endHtml = '</ht' + 'ml>'                  $htmlContent = @" <! Định dạng html DOCTYPE> <html lang="en"> <đầu> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <tiêu đề của>bảng điều khiển Trạng thái Chứng chỉ Khởi động An toàn</title> <script src="https://cdn.jsdelivr.net/npm/chart.js">$endScript <cách thiết> *{box-sizing:border-box; lề:0; đệm:0} body{font-family:'Segoe UI',Tahoma,sans-serif; nền:#f0f2f5; color:#333} .header{background:linear-gradient(135deg,#1a237e.#0d47a1); màu:#fff; đệm:20px 30px} .header h1{font-size:1.6em; margin-bottom:5px} .header .meta{font-size:.85em; opacity:.9} .container{max-width:1400px; margin:0 auto; đệm:20px} .cards{display:grid; lưới-mẫu-cột:repeat(auto-fill,minmax(170px,1fr)); khoảng cách:12px; margin:20px 0} .card{background:#fff; biên giới bán kính:10px; đệm:15px; box-shadow:0 2px 8px rgba(0,0,0,,08); viền trái:4px rắn #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; trọng lượng phông chữ:700} .card .label{font-size:.8em; màu:#666; margin-top:4px} .card .pct{font-size:.75em; màu:#888} .section{background:#fff; biên giới bán kính:10px; đệm:20px; margin:15px 0; box-shadow:0 2px 8px rgba(0,0,0,.08)} .section h2{font-size:1.2em; màu:#1a237e; margin-bottom:10px; con trỏ:con trỏ; 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; khoảng cách:20px; margin:20px 0} .chart-box{background:#fff; biên giới bán kính:10px; đệm:20px; box-shadow:0 2px 8px rgba(0,0,0,.08)} bảng{width:100%; viền-thu gọn:thu gọn; cỡ phông:.85em} th{background:#e8eaf6; đệm:8px 10px; căn chỉnh văn bản:trái; vị trí:dính; top:0; z-index:1} td{padding:6px 10px; border-bottom:1px solid #eee} tr:hover{background:#f5f5f5} .badge{display:inline-block; đệm:2px 8px;bán kính viền:10px; cỡ phông:,75em; trọng lượng phông chữ:700} .badge-success{background:#d4edda; color:#155724} .badge-danger{background:#f8d7da; color:#721c24} .badge-warning{background:#fff3cd; color:#856404} .badge-info{background:#d1ecf1; color:#0c5460} .top-link{float:right; cỡ phông:.8em; màu:#1a237e; text-decoration:none} .footer{text-align:center; đệm:20px; màu:#999; cỡ phông:.8em} a{color:#1a237e}$endStyle $endHead <toàn thân> <div class="header">     <h1 trên bảng>trạng thái khởi động an toàn của chứng chỉ</h1>     <div class="meta">Generated: $($stats. ReportGeneratedAt) | Tổng số Thiết bị: $($c.Total.ToString("N0")) | Bộ chứa Duy nhất: $($stAllBuckets.Count)</div> </div> <div class="container">

<!-- Thẻ KPI - có thể bấm được, được liên kết với các mục --> <div class="cards">     <a 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; nền:#dc3545; màu:#fff; đệm:1px 6px; biên giới bán kính:8px; cỡ phông:,65em; độ dày phông:700">PRIMARY</div><div class="value" style="color:#dc3545">$($stNotUptodate.ToString("N0"))</div><div class="label">NOT UPDATED><6 /div><class="pct">$($stats. PercentNotUptodate)% - CẦN HÀNH ĐỘNG><0 /div></a><3     <a 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; nền:#28a745; màu:#fff; đệm:1px 6px; biên giới bán kính:8px; cỡ phông:,65em; trọng lượng phông:700">PRIMARY><8 /div><div class="value" style="color:#28a745">$($c.Updated.ToString("N0"))</div><div class="label">Updated><6 /div><div class="pct">$($stats. PercentCertUpdated)%</div></a><3     <a 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; nền:#6c757d; màu:#fff; đệm:1px 6px; biên giới bán kính:8px; cỡ phông:,65em; font-weight:700">PRIMARY><8 /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)}else{0})% - Ngoài Phạm vi><0 /div></a><3     <a 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">Needs Reboot><2 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.NeedsReboot/$c.Total)*100,1)}else{0})% - đang chờ khởi động lại><6 /div></a><9     <a 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><$c div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.UpdatePending/$c.Total)*100,1)}else{0})% - Chính sách/WinCS được áp dụng, đang chờ cập><2 /div></a><5     <a 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 /di><v class="pct">$(if($c.Total -gt 0){[math]::Round(($c.RolloutInProgress/$c.Total)*100,1)}else{0})%</div></a><11     <a 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)% - An toàn để triển khai><24 /div></a><27     <a 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)}else{0})%</div></a><3     <a 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)% - phải kiểm><6 /div></a><9     <a 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)% - Tương tự như thất bại><2 /div></a><5     <a 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})% - Bị chặn><8 /div></a><91     <a 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. Tạm dừng</div><div class="pct">$(if($c.Total -gt 0){[math]::Round($c.TempPaused/$c.Total)*100,1)}else{0})%</div></a>     <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">Known Issues><6 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.WithKnownIssues/$c.Total)*100,1)}else{0})%</div></a><3     <a 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>     <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)% - Lỗi UEFI</div></a>     ><6 a 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. Lỗi</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TempFailures/$c.Total)*100,1)}else{0})%</div></a>     <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)}else{0})%</div></a><3 </div>

<!-- Vận tốc Triển khai & Cấp chứng chỉ --> <div id="s-velocity" style="display:grid; grid-template-columns:1fr 1fr; khoảng cách:20px; margin:15px 0"> <div class="section" style="margin:0">     <h2>&#128197; Vận tốc triển</h2 cho>     ><2 div class="section-body open"><3         ><4 div style="font-size:2,5em; trọng lượng phông:700; color:#28a745"><5 $($c.Updated.ToString("N0"))><6 /div>         ><8 div style="color:#666"><9 devices updated out of $($c.Total.ToString("N0"))</div>         <div style="margin:10px 0; nền:#e8eaf6; chiều cao:20px; biên giới bán kính:10px; tràn:hidden"><div style="background:#28a745; chiều cao:100%; width:$($stats. PercentCertUpdated)%; border-radius:10px"></div></div>         <div style="font-size:.8em; color:#888">$($stats. PercentCertUpdated)% hoàn thành</div>         <div style="margin-top:10px; đệm:10px; nền:#f8f9fa; biên giới bán kính:8px; cỡ phông:,85em">             <div><>Số>Còn lại:</strong> $($stNotUptodate.ToString("N0")) cần phải hành động</div >             <div><>chặn mạnh mẽ:</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated) thiết bị (lỗi + vĩnh viễn + tắt tác vụ)</div>             <div><>mạnh để triển khai:</strong> $($stSafeList.ToString("N0")) thiết bị (cùng một bộ chứa thành công)</div>             $velocityHtml         </div>     </div> </div> <div class="section" style="margin:0; viền trái:4px rắn #dc3545">     <h2 style="color:#dc3545">&#9888; Certificate Expiry Countdown</h2>     <div class="section-body open">         <div style="display:flex; khoảng cách:15px; margin-top:10px">             <div style="text-align:center; đệm:15px; biên giới bán kính:8px; min-width:120px; background:linear-gradient(135deg,#fff5f5,#ffe0e0); viền:2px rắn #dc3545; flex:1">                 <div style="font-size:.65em; màu:#721c24; biến đổi văn bản:chữ hoa; font-weight:bold">&#9888; FIRST TO EXPIRE</div>                 ><4 div style="font-size:.85em; font-weight:bold; màu:#dc3545; margin:3px 0"><5 KEK CA 2011</div>                 ><8 div id="daysKek" style="font-size:2,5em; trọng lượng phông:700; màu:#dc3545; chiều cao dòng:1"><9 $daysToKek><0 /div cho><1                 <div style="font-size:.8em; color:#721c24">days (Jun 24, 2026)</div><5             </div><7             <div style="text-align:center; đệm:15px; biên giới bán kính:8px; min-width:120px; background:linear-gradient(135deg,#fffef5,#fff3cd); viền:2px rắn #ffc107; flex:1">                 ><00 div style="font-size:.65em; color:#856404; biến đổi văn bản:chữ hoa; font-weight:bold"><01 UEFI CA 2011</div>                 ><04 div id="daysUefi" style="font-size:2.2em; trọng lượng phông:700; color:#856404; chiều cao đường:1; margin:5px 0"><05 $daysToUefi</div>                 ><08 div style="font-size:.8em; color:#856404"><09 days (Jun 27, 2026)><10 /div>             ><12 /div>             ><14 div style="text-align:center; đệm:15px; biên giới bán kính:8px; min-width:120px; background:linear-gradient(135deg,#f0f8ff,#d4edff); viền:2px rắn #0078d4; flex:1"><15                 ><16 div style="font-size:.65em; màu:#0078d4; biến đổi văn bản:chữ hoa; font-weight:bold"><17 PCA cho Windows</div>                 ><20 div id="daysPca" style="font-size:2.2em; trọng lượng phông:700; màu:#0078d4; chiều cao đường:1; margin:5px 0"><21 $daysToPca><2 /div><3                 ><24 div style="font-size:.8em; color:#0078d4"><25 days (Oct 19, 2026)><26 /div><7             ><28 /div><9         ><30 /div><1         ><32 div style="margin-top:15px; đệm:10px; nền:#f8d7da; biên giới bán kính:8px; cỡ phông:,85em; viền trái:4px rắn #dc3545"><33             ><34 cập>&#9888; CRITICAL:><37 /strong> Tất cả các thiết bị phải được cập nhật trước khi hết hạn chứng chỉ. Thiết bị không được cập nhật đúng hạn chót không thể áp dụng các bản cập nhật Bảo mật trong tương lai cho Trình quản lý Khởi động và Khởi động An toàn sau khi hết hạn.</div>     </div> </div> </div>

<!-- đồ --> <div class="charts">     <div class="chart-box"><h3>Deployment Status</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>

$(if ($historyData.Count -ge 1) { "<!-- đồ xu hướng lịch sử --> <div class='section'>     <h2 onclick='"toggle('d-trend')'">&#128200; Cập nhật tiến độ theo thời gian <một class='top-link' href='#'>&#8593; Top</a></h2>     <div id='d-trend' class='section-body open'>         <bảng tùy biến id='trendChart' height='120'></canvas>         <div style='font-size:.75em; màu:#888; margin-top:5px'>Solid lines = actual data$(if ($historyData.Count -ge 2) { " | Đường nét đứt = được chiếu (gấp hai số mũ: 2&#x2192;4&#x2192;8&#x2192;16... thiết bị mỗi đợt)" } khác { " | Chạy lại phép tổng hợp vào ngày mai để xem các đường xu hướng và chiếu" })</div>     </div> </div>" })

<!-- xuống CSV --> <div class="section">     <h2 onclick="toggle('dl-csv')">&#128229; Tải xuống Toàn bộ Dữ liệu (CSV cho Excel) <một class="top-link" href="#">Top</a></h2>     <div id="dl-csv" class="section-body open" style="display:flex; flex-wrap:wrap; gap:5px">         <a href="SecureBoot_not_updated_$timestamp.csv" style="display:inline-block; nền:#dc3545; màu:#fff; đệm:6px 14px; viền bán kính:5px; text-decoration:none; font-size:.8em">Not Updated ($($stNotUptodate.ToString("N0")))</a >         <a href="SecureBoot_errors_$timestamp.csv" style="display:inline-block; nền:#dc3545; màu:#fff; đệm:6px 14px; viền bán kính:5px; text-decoration:none; font-size:.8em">Errors ($($c.WithErrors.ToString("N0")))</a><2         <a href="SecureBoot_action_required_$timestamp.csv" style="display:inline-block; nền:#fd7e14; màu:#fff; đệm:6px 14px; viền bán kính:5px; text-decoration:none; font-size:.8em">Action Required ($($c.ActionReq.ToString("N0")))</a><6         <a href="SecureBoot_known_issues_$timestamp.csv" style="display:inline-block; nền:#dc3545; màu:#fff; đệm:6px 14px; viền bán kính:5px; text-decoration:none; font-size:.8em">Known Issues ($($c.WithKnownIssues.ToString("N0")))</a>         <a href="SecureBoot_task_disabled_$timestamp.csv" style="display:inline-block; nền:#dc3545; màu:#fff; đệm:6px 14px; viền bán kính:5px; text-decoration:none; font-size:.8em">Task Disabled ($($c.TaskDisabled.ToString("N0")))</a>         <a href="SecureBoot_updated_devices_$timestamp.csv" style="display:inline-block; nền:#28a745; màu:#fff; đệm:6px 14px; viền bán kính:5px; text-decoration:none; font-size:.8em">Updated ($($c.Updated.ToString("N0")))</a>         <a href="SecureBoot_Summary_$timestamp.csv" style="display:inline-block; nền:#6c757d; màu:#fff; đệm:6px 14px; viền bán kính:5px; text-decoration:none; cỡ phông:,8em">Tóm</a bản>         <div style="width:100%; cỡ phông:,75em; màu:#888; margin-top:5px">CSV đang mở trong Excel. Khả dụng khi được lưu trữ trên máy chủ web.</div>     </div> </div>

<!-- chi tiết nhà sản xuất --> <div class="section">     <h2 onclick="toggle('mfr')">By Manufacturer <a class="top-link" href="#">Top</a></h2><1     <div id="mfr" class="section-body open">     <bảng><có><tr><thứ><1 Nhà sản xuất><2 /th><thứ><5 Tổng><6 /th><thứ><9 Cập nhật><9><0 /th><th><3 High Confidence><4 /th><th><7 Action Required><8 /th></tr></thead><3     <tbody><5 $mfrTableRows><6 /tbody></table><9     </div><1 </div>

<!-- Mục Thiết bị (tải xuống 200 thiết bị nội tuyến + CSV đầu tiên) --> <div class="section" id="s-err">     <h2 onclick="toggle('d-err')">&#128308; Thiết bị có Lỗi ($($c.WithErrors.ToString("N0"))) <a 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; Sự cố Đã biết ($($c.WithKnownIssues.ToString("N0"))) <a 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; Thiếu KEK - Sự kiện 1803 ($($c.WithMissingKEK.ToString("N0"))) <a 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; Action Bắt buộc ($($c.ActionReq.ToString("N0"))) <a 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; Dưới Quan sát ($($c.UnderObs.ToString("N0"))) <một 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; Không cập nhật ($($stNotUptodate.ToString("N0"))) <một 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; Đã tắt Tác vụ ($($c.TaskDisabled.ToString("N0"))) >&#8593; 5 một 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; Lỗi tạm thời ($($c.TempFailures.ToString("N0"))) <a 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; Lỗi Vĩnh viễn / Không Được hỗ trợ ($($c.PermFailures.ToString("N0"))) <a 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; Update Pending ($($c.UpdatePending.ToString("N0"))) - Chính sách/WinCS Được áp dụng, Đang chờ cập <một 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 or WinCS key is applied but UEFICA2023Status is still NotStarted, InProgress, or null.</p>$tblUpdatePending</div> </div> <div class="section" id="s-rip">     <h2 onclick="toggle('d-rip')" style="color:#17a2b8">&#128309; Đang triển khai ($($c.RolloutInProgress.ToString("N0"))) <a 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"))) - Ngoài Phạm vi <một 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ập nhật Thiết bị ($($c.Updated.ToString("N0"))) <a 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ập nhật - Cần Khởi động lại ($($c.NeedsReboot.ToString("N0"))) <a class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-nrb" class="section-body">$tblNeedsReboot</div> </div>

<div class="footer">Bảng điều khiển Triển khai Chứng chỉ Khởi động An toàn | Đã tạo $($stats. ReportGeneratedAt) | StreamingMode | Bộ nhớ đỉnh: ${stPeakMemMB} MB</div> </div><!-- /container -->

<lệnh> function toggle(id){var e=document.getElementById(id); e.classList.toggle('open')} function 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:['Update','Update Pending','High Confidence','Under Observation','Action Required','Temp. Paused','Not Supported','SecureBoot OFF','With Errors'],datasets:[{data:[$($c.Updated),$($c.UpdatePending),$($c.HighConf),$($c.UnderObs),$($c.ActionReq),$($c.TempPaused),$($c.NotSupported),$($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:'Với Lỗi',dữ liệu:[$mfrWithErrors],backgroundColor:'#dc3545'}]},options:{responsive:true,scales:{x:{stacked:true},y:{stacked:true}},plugins:{legend:{position:'top'}}}); Biểu đồ xu hướng lịch sử if (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); if (projLen > 0) { projLine[histLen-1] = actualUpdated[histLen-1]; projLine = projLine.concat(projData); projNotUpdLine[histLen-1] = actualNotUpdated[histLen-1]; projNotUpdLine = projNotUpdLine.concat(projNotUpdData); } tập dữ liệu var = [     {label:'Đã cập nhật',dữ liệu: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} ]; if (projLen > 0) {     datasets.push({label:'Projected Updated (2x doubling)',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'}}}); } Đếm ngược động (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))         # Luôn giữ một bản sao "Mới nhất" ổn định để người quản trị không cần theo dõi dấu thời gian         $latestPath = Join-Path $OutputPath "SecureBoot_Dashboard_Latest.html"         Copy-Item $htmlPath $latestPath -Force         $stTotal = $streamSw.Elapsed.TotalSeconds         # Lưu tệp kê khai để tăng chế độ (phát hiện nhanh không thay đổi khi chạy tiếp theo)         if ($IncrementalMode -or $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 "Lưu tệp kê khai cho chế độ tăng dần..." -ForegroundColor Gray             foreach ($jf in $jsonFiles) {                 $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{                     LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o")                     Kích cỡ = $jf. Chiều dài                 }             }             Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath             Write-Host " Tệp kê khai đã lưu cho tệp $($stNewManifest.Count) " -ForegroundColor DarkGray         }         # DỌN DẸP LƯU GIỮ         # Orchestrator thư mục có thể tái sử dụng (Aggregation_Current): chỉ giữ chạy mới nhất (1)         # Quản trị chạy thủ công / thư mục khác: giữ cho 7 lần chạy gần nhất         # CSV tóm tắt KHÔNG BAO GIỜ bị xóa — chúng rất nhỏ (~ 1 KB) và là nguồn sao lưu cho lịch sử xu hướng         $outputLeaf = Split-Path $OutputPath -Leaf         $retentionCount = if ($outputLeaf -eq 'Aggregation_Current') { 1 } khác { 7 }         # Tệp tiền tố an toàn để dọn sạch (ảnh tức thời cho mỗi lần chạy không lâu)         $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_'         )         # Tìm tất cả các dấu thời gian duy nhất từ các tệp có thể làm sạch chỉ         $cleanableFiles = Get-ChildItem $OutputPath -File -EA SilentlyContinue |             Where-Object { $f = $_. Tên; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Đếm -gt 0 }         $allTimestamps = @($cleanableFiles | ForEach-Object {             nếu ($_. Tên -match '(\d{8}-\d{6})') { $Matches[1] }         } | Sort-Object -Duy nhất -Giảm dần)         if ($allTimestamps.Count -gt $retentionCount) {             $oldTimestamps = $allTimestamps | Select-Object -Bỏ qua $retentionCount             $removedFiles = 0; $freedBytes = 0             foreach ($oldTs in $oldTimestamps) {                 foreach ($prefix in $cleanupPrefixes) {                     $oldFiles = Get-ChildItem $OutputPath -Tệp -Lọc "${prefix}${oldTs}*" -EA SilentlyContinue                     foreach ($f in $oldFiles) {                         $freedBytes += $f.Độ dài                         Remove-Item $f.FullName -Force -EA SilentlyContinue                         $removedFiles++                     }                 }             }             $freedMB = [toán học]::Round($freedBytes / 1MB, 1)             Write-Host "Dọn dẹp lưu giữ: xóa các tệp $removedFiles khỏi $($oldTimestamps.Count) cũ chạy, giải phóng ${freedMB} MB (giữ lại lần cuối $retentionCount + tất cả Tóm tắt/NotUptodate CSVs)" -ForegroundColor DarkGray         }         Write-Host "'n$("=" * 60)" -ForegroundColor Cyan         Write-Host "STREAMING AGGREGATION COMPLETE" -ForegroundColor Green         Write-Host ("=" * 60) -ForegroundColor Cyan         Write-Host " Tổng số Thiết bị: $($c.Total.ToString("N0"))" -ForegroundColor White         Write-Host " CHƯA CẬP NHẬT: $($stNotUptodate.ToString("N0")) ($($stats. PercentNotUptodate)%)" -ForegroundColor $(if ($stNotUptodate -gt 0) { "Yellow" } else { "Green" })         Write-Host " Đã cập nhật: $($c.Updated.ToString("N0")) ($($stats. PercentCertUpdated)%)" -ForegroundColor Green         Write-Host " Với Lỗi: $($c.WithErrors.ToString("N0"))" -ForegroundColor $(if ($c.WithErrors -gt 0) { "Red" } else { "Green" })         Write-Host " Bộ nhớ đỉnh: ${stPeakMemMB} MB" -ForegroundColor Cyan         Write-Host " Thời gian: $([toán học]::Round($stTotal/60,1)) phút" -ForegroundColor White         Write-Host " Bảng điều khiển: $htmlPath" -ForegroundColor White         return [PSCustomObject]$stats     }     #endregion THỨC PHÁT TRỰC TUYẾN } người khác {     Write-Error "Không tìm thấy đường dẫn nhập: $InputPath"     thoát 1 }                                                      

Bạn cần thêm trợ giúp?

Bạn muốn xem các tùy chọn khác?

Khám phá các lợi ích của gói đăng ký, xem qua các khóa đào tạo, tìm hiểu cách bảo mật thiết bị của bạn và hơn thế nữa.