Salin dan tempelkan contoh skrip ini dan ubah sesuai kebutuhan untuk lingkungan Anda:

<# . SINOPSIS     Menggabungkan data JSON status Boot Aman dari beberapa perangkat ke dalam laporan ringkasan.

. DESKRIPSI     Membaca file JSON status Boot Aman yang dikumpulkan dan menghasilkan:     - Dasbor HTML dengan bagan dan pemfilteran     - Ringkasan menurut ConfidenceLevel     - Analisis bucket perangkat unik untuk strategi      pengujian     Mendukung:     - File per mesin: HOSTNAME_latest.json (disarankan)     - File JSON      tunggal     Secara otomatis deduplikat oleh HostName, mempertahankan CollectionTime terbaru.     Secara default, hanya menyertakan perangkat dengan kepercayaan diri "Action Req" atau "High"     untuk fokus pada wadah yang dapat ditindak lanjuti. Gunakan -IncludeAllConfidenceLevels untuk menimpa.

. PARAMETER InputPath     Jalur ke file JSON:     - Folder: Membaca semua *_latest.json file (atau *.json jika tidak ada file _latest)     - File: Membaca satu file JSON

. PARAMETER OutputPath     Jalur untuk laporan yang dihasilkan (default: .\SecureBootReports)

. CONTOH     # Agregat dari folder file per mesin (disarankan)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$"     # Baca: \\contoso\SecureBootLogs$\*_latest.json

. CONTOH     # Lokasi output kustom     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -OutputPath "C:\Reports\SecureBoot"

. CONTOH     # Sertakan hanya Req Tindakan dan Kepercayaan diri tinggi (perilaku default)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$"     # Tidak termasuk: Observasi, Dijeda, Tidak Didukung

. CONTOH     # Sertakan semua tingkat kepercayaan (mengesampingkan filter)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeAllConfidenceLevels

. CONTOH     # Filter tingkat kepercayaan kustom     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeConfidenceLevels @("Action Req", "High", "Observation")

. CONTOH     # SKALA PERUSAHAAN: Mode penambahan - hanya proses file yang diubah (berjalan cepat selanjutnya)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode     # Jalankan pertama: Muat penuh ~2 jam untuk perangkat 500K     # Selanjutnya berjalan: Detik jika tidak ada perubahan, menit untuk delta

. CONTOH     # Lewati HTML jika tidak ada yang berubah (tercepat untuk pemantauan)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode -SkipReportIfUnchanged     # Jika tidak ada file yang berubah sejak dijalankan terakhir: ~5 detik

. CONTOH     # Mode ringkasan saja - lewati tabel perangkat besar (1-2 menit vs 20+ menit)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -SummaryOnly     # Menghasilkan CSV tetapi melewati dasbor HTML dengan tabel perangkat penuh

. CATATAN     Pasangkan dengan Detect-SecureBootCertUpdateStatus.ps1 untuk penyebaran perusahaan.Lihat GPO-DEPLOYMENT-GUIDE.md untuk panduan penggunaan penuh.     Perilaku default tidak termasuk perangkat Observasi, Dijeda, dan Tidak Didukung     untuk memfokuskan pelaporan hanya pada wadah perangkat yang dapat ditindaklanjuti.#>

param(     [Parameter(Wajib = $true)]     [string]$InputPath,          [Parameter(Wajib = $false)]     [string]$OutputPath = ".\SecureBootReports",          [Parameter(Wajib = $false)]     [string]$ScanHistoryPath = ".\SecureBootReports\ScanHistory.json",          [Parameter(Wajib = $false)]     [string]$RolloutStatePath, # Jalur ke RolloutState.json untuk mengidentifikasi perangkat      InProgress     [Parameter(Wajib = $false)]     [string]$RolloutSummaryPath, # Jalur ke SecureBootRolloutSummary.json dari Orkestrator (berisi data proyeksi)          [Parameter(Wajib = $false)]     [string[]$IncludeConfidenceLevels = @("Tindakan Diperlukan", "Kepercayaan Diri Tinggi"), # Hanya menyertakan tingkat kepercayaan ini (default: wadah yang dapat ditindaklanjuti saja)          [Parameter(Wajib = $false)]     [switch]$IncludeAllConfidenceLevels, # Override filter untuk menyertakan semua tingkat      kepercayaan     [Parameter(Wajib = $false)]     [sakelar]$SkipHistoryTracking,          [Parameter(Wajib = $false)]     [sakelar]$IncrementalMode, # Aktifkan pemrosesan delta - hanya muat file yang diubah sejak dijalankan      terakhir     [Parameter(Wajib = $false)]     [string]$CachePath, # Jalur ke direktori cache (default: OutputPath\.cache)          [Parameter(Wajib = $false)]     [int]$ParallelThreads = 8, # Jumlah utas paralel untuk pemuatan file (PS7+)          [Parameter(Wajib = $false)]     [sakelar]$ForceFullRefresh, # Paksa muat ulang penuh bahkan dalam mode      penambahan     [Parameter(Wajib = $false)]     [switch]$SkipReportIfUnchanged, # Lewati pembuatan HTML/CSV jika tidak ada file yang berubah (hanya statistik output)          [Parameter(Wajib = $false)]     [sakelar]$SummaryOnly, # Hasilkan statistik ringkasan saja (tidak ada tabel perangkat besar) - jauh lebih cepat          [Parameter(Wajib = $false)]     [switch]$StreamingMode # Mode efisien memori: potongan proses, menulis CSV secara bertahap, hanya menyimpan ringkasan dalam memori )

# Peningkatan otomatis ke PowerShell 7 jika tersedia (6x lebih cepat untuk kumpulan data besar) if ($PSVersionTable.PSVersion.Major -lt 7) {     $pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source     if ($pwshPath) {         Write-Host "PowerShell $($PSVersionTable.PSVersion) terdeteksi - meluncurkan ulang dengan PowerShell 7 untuk pemrosesan yang lebih cepat..." -ForegroundColor Yellow         # Susun ulang daftar argumen dari parameter terikat         $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $MyInvocation.MyCommand.Path)         foreach ($key di $PSBoundParameters.Keys) {             $val = $PSBoundParameters[$key]             if ($val -is [switch]) {                 jika ($val. IsPresent) { $relaunchArgs += "-$key" }             } elseif ($val -is [array]) {                 $relaunchArgs += "-$key"                 $relaunchArgs += ($val -join ',')             } lain {                 $relaunchArgs += "-$key"                 $relaunchArgs += "$val"             }         }         & $pwshPath @relaunchArgs         keluar dari $LASTEXITCODE     } }

$ErrorActionPreference = "Lanjutkan" $timestamp = Get-Date -Format "yyyyMdd-HHmmss" $scanTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Sampel Penyebaran dan Pemantauan"

# Catatan: Skrip ini tidak memiliki dependensi pada skrip lain. # Untuk peralatan lengkap, unduh dari: $DownloadUrl -> $DownloadSubPage

Penyiapan #region Write-Host "=" * 60 -ForegroundColor Sian Write-Host "Agregasi Data Boot Aman" -ForegroundColor Cyan Write-Host "=" * 60 -ForegroundColor Sian

# Buat direktori output if (-not (Test-Path $OutputPath)) {     New-Item -Direktori ItemType -Path $OutputPath -Force | Out-Null }

# Memuat data - mendukung format CSV (warisan) dan JSON (asli) Write-Host "'n Memuat data dari: $InputPath" -ForegroundColor Yellow

# Fungsi helper untuk menormalkan objek perangkat (menangani perbedaan nama bidang) fungsi Normalize-DeviceRecord {     param($device)          # Handle Hostname vs HostName (JSON menggunakan Hostname, CSV menggunakan HostName)     jika ($device. PSObject.Properties['Hostname'] -and -not $device. PSObject.Properties['HostName']) {         $device | Add-Member -NotePropertyName 'HostName' -NotePropertyValue $device. Nama host -Force     }          # Handle Confidence vs ConfidenceLevel (JSON menggunakan Confidence, CSV uses ConfidenceLevel)     # ConfidenceLevel adalah nama bidang resmi - map Confidence to it     jika ($device. PSObject.Properties['Confidence'] -and -not $device. PSObject.Properties['ConfidenceLevel']) {         $device | Add-Member -NotePropertyName 'ConfidenceLevel' -NotePropertyValue $device. Kepercayaan diri -Force     }          # Lacak status pembaruan melalui Event1808Count OR UEFICA2023Status="Updated"     # Ini memungkinkan pelacakan berapa banyak perangkat dalam setiap wadah kepercayaan telah diperbarui     $event 1808 = 0     jika ($device. PSObject.Properties['Event1808Count']) {         $event 1808 = [int]$device. Event1808Count     }     $uefiCaUpdated = $false     jika ($device. PSObject.Properties['UEFICA2023Status'] -dan $device. UEFICA2023Status -eq "Diperbarui") {         $uefiCaUpdated = $true     }          if ($event 1808 -gt 0 -or $uefiCaUpdated) {         # Tandai sebagai diperbarui untuk dasbor/logika peluncuran - tapi JANGAN mengesampingkan ConfidenceLevel         $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $true -Force     } lain {         $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $false -Force                  # ConfidenceKlasifikasi tingkat tinggi:         # - "Kepercayaan Diri Tinggi", "Di bawah Observasi...", "Dijeda Sementara...", "Tidak Didukung..." = gunakan sebagaimana adanya         # - Segala sesuatu yang lain (null, kosong, "UpdateType:...", "Unknown", "N/A") = jatuh ke Tindakan Diperlukan dalam counter         # Tidak diperlukan normalisasi — counter streaming cabang lain menanganinya     }          # Handle OEMManufacturerName vs WMI_Manufacturer (JSON menggunakan OEM*, warisan menggunakan WMI_*)     jika ($device. PSObject.Properties['OEMManufacturerName'] -and -not $device. PSObject.Properties['WMI_Manufacturer']) {         $device | Add-Member -NotePropertyName 'WMI_Manufacturer' -NotePropertyValue $device. OEMManufacturerName -Force     }          # Tangani OEMModelNumber vs WMI_Model     jika ($device. PSObject.Properties['OEMModelNumber'] -and -not $device. PSObject.Properties['WMI_Model']) {         $device | Add-Member -NotePropertyName 'WMI_Model' -NotePropertyValue $device. OEMModelNumber -Force     }          # Tangani FirmwareVersion vs BIOSDescription     jika ($device. PSObject.Properties['FirmwareVersion'] -and -not $device. PSObject.Properties['BIOSDescription']) {         $device | Add-Member -NotePropertyName 'BIOSDescription' -NotePropertyValue $device. FirmwareVersion -Force     }          $device pengembalian }

#region Pemrosesan Penambahan / Manajemen Singgahan # Jalur cache penyiapan if (-not $CachePath) {     $CachePath = Join-Path $OutputPath ".cache" } $manifestPath = Join-Path $CachePath "FileManifest.json" $deviceCachePath = Join-Path $CachePath "DeviceCache.json"

# Fungsi manajemen singgahan fungsi Get-FileManifest {     param([string]$Path)     if (Test-Path $Path) {         coba {             $json = Get-Content $Path -Raw | ConvertFrom-Json             # Konversi PSObject ke hashtable (kompatibel PS5.1 - PS7 memiliki -AsHashtable)             $ht = @{}             $json. PSObject.Properties | ForEach-Object { $ht[$_. Nama] = $_. Nilai }             $ht pengembalian         } tangkap {             return @{}         }     }     return @{} }

fungsi Save-FileManifest {     param([hashtable]$Manifest, [string]$Path)     $dir = Split-Path $Path -Parent     if (-not (Test-Path $dir)) {         New-Item -Direktori ItemType -Path $dir -Force | Out-Null     }     $Manifest | ConvertTo-Json -Kedalaman 3 -Kompresi | Set-Content $Path -Force }

fungsi Get-DeviceCache {     param([string]$Path)     if (Test-Path $Path) {         coba {             $cacheData = Get-Content $Path -Raw | ConvertFrom-Json             Write-Host " Cache perangkat yang dimuat: perangkat $($cacheData.Count) " -ForegroundColor DarkGray             $cacheData pengembalian         } tangkap {             Write-Host " Cache rusak, akan dibangun ulang" -ForegroundColor Yellow             return @()         }     }     return @() }

fungsi Save-DeviceCache {     param($Devices, [string]$Path)     $dir = Split-Path $Path -Parent     if (-not (Test-Path $dir)) {         New-Item -Direktori ItemType -Path $dir -Force | Out-Null     }     # Konversi ke array dan simpan     $deviceArray = @($Devices)     $deviceArray | ConvertTo-Json -Kedalaman 10 -Kompresi | Set-Content $Path -Force     Write-Host " Cache perangkat yang disimpan: perangkat $($deviceArray.Count) " -ForegroundColor DarkGray }

fungsi Get-ChangedFiles {     param(         [System.IO.FileInfo[]]$AllFiles,         [hashtable]$Manifest     )          $changed = [System.Collections.ArrayList]::new()     $unchanged = [System.Collections.ArrayList]::new()     $newManifest = @{}          # Menyusun pencarian yang tidak peka huruf besar kecil dari manifes (dinormalkan ke huruf kecil)     $manifestLookup = @{}     foreach ($mk di $Manifest.Keys) {         $manifestLookup[$mk. ToLowerInvariant()] = $Manifest[$mk]     }          foreach ($file di $AllFiles) {         $key = $file. FullName.ToLowerInvariant() # Normalisasi jalur ke huruf kecil         $lwt = $file. LastWriteTimeUtc.ToString("o")         $newManifest[$key] = @{             LastWriteTimeUtc = $lwt             Ukuran = $file. Panjang         }                  if ($manifestLookup.ContainsKey($key)) {             $cached = $manifestLookup[$key]             jika ($cached. LastWriteTimeUtc -eq $lwt -and $cached. Ukuran -eq $file. Panjang) {                 [batal]$unchanged. Tambahkan($file)                 Lanjutkan             }         }         [batal]$changed. Add($file)     }          return @{         Diubah = $changed         Tidak berubah = $unchanged         NewManifest = $newManifest     } }

# Pemuatan file paralel ultra cepat menggunakan pemrosesan kumpulan fungsi Load-FilesParallel {     param(         [System.IO.FileInfo[]]$Files,         [int]$Threads = 8     )

$totalFiles = $Files. Menghitung     # Gunakan kumpulan file ~1000 masing-masing untuk kontrol memori yang lebih baik     $batchSize = [matematika]::Min(1000, [matematika]::Plafon($totalFiles / [matematika]::Max(1, $Threads)))     $batches = [System.Collections.Generic.List[object]]::new()     

for ($i = 0; $i -lt $totalFiles; $i += $batchSize) {         $end = [matematika]::Min($i + $batchSize, $totalFiles)         $batch = $Files[$i.. ($end-1)]         $batches. Add($batch)     }     Write-Host " ($($batches. Hitung) kumpulan ~$batchSize file masing-masing)" -NoNewline -ForegroundColor DarkGray     $flatResults = [System.Collections.Generic.List[object]]::new()     # Periksa apakah PowerShell 7+ paralel tersedia     $canParallel = $PSVersionTable.PSVersion.Major -ge 7     if ($canParallel -and $Threads -gt 1) {         # PS7+: Kumpulan proses secara paralel         $results = $batches | ForEach-Object -ThrottleLimit $Threads -Parallel {             $batchFiles = $_             $batchResults = [System.Collections.Generic.List[object]]::new()             foreach ($file di $batchFiles) {                 coba {                     $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json                     $batchResults.Add($content)                 } tangkap { }             }             $batchResults.ToArray()         }         foreach ($batch dalam $results) {             if ($batch) { foreach ($item in $batch) { $flatResults.Add($item) } }         }     } lain {         # PS5.1 mundur: Pemrosesan berurutan (masih cepat untuk file <10K)         foreach ($file dalam $Files) {             coba {                 $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json                 $flatResults.Add($content)             } tangkap { }         }     }     return $flatResults.ToArray() } #endregion                         

$allDevices = @() if (Test-Path $InputPath -PathType Leaf) {     # File JSON tunggal     if ($InputPath -like "*.json") {         $jsonContent = Get-Content -Jalur $InputPath -Raw | ConvertFrom-Json         $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ }         Write-Host "Memuat rekaman $($allDevices.Count) dari file"     } lain {         Write-Error "Hanya format JSON yang didukung. File harus memiliki ekstensi .json."         keluar 1     } } elseif (Test-Path $InputPath -PathType Container) {     # Folder - hanya JSON     $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue |                    Where-Object { $_. Nama -notmatch "ScanHistory|RolloutState|RolloutPlan" })          # Pilih *_latest.json file jika ada (mode per-mesin)     $latestJson = $jsonFiles | Where-Object { $_. Nama -like "*_latest.json" }     if ($latestJson.Count -gt 0) { $jsonFiles = $latestJson }          $totalFiles = $jsonFiles.Count          if ($totalFiles -eq 0) {         Write-Error "Tidak ada file JSON yang ditemukan di: $InputPath"         keluar 1     }          Write-Host "Ditemukan $totalFiles file JSON" -ForegroundColor Gray          # Fungsi helper untuk mencocokkan tingkat kepercayaan (menangani bentuk pendek dan penuh)     # Didefinisikan lebih awal sehingga jalur StreamingMode dan normal dapat menggunakannya     fungsi Test-ConfidenceLevel {         param([string]$Value, [string]$Match)         if ([string]:::IsNullOrEmpty($Value)) { return $false }         sakelar ($Match) {             "HighConfidence" { return $Value -eq "High Confidence" }             "UnderObservation" { return $Value -like "Under Observation*" }             "ActionRequired" { return ($Value -like "*Action Diperlukan*" -or $Value -eq "Action Diperlukan") }             "TemporarilyPaused" { return $Value -like "Temporarily Paused*" }             "NotSupported" { return ($Value -like "Not Supported*" -or $Value -eq "Not Supported") }             default { return $false }         }     }          mode streaming #region - Pemrosesan memori efisien untuk kumpulan data besar     # Selalu gunakan StreamingMode untuk pemrosesan yang efisien memori dan dasbor gaya baru     if (-not $StreamingMode) {         Write-Host "Auto-enabling StreamingMode (new-style dashboard)" -ForegroundColor Yellow         $StreamingMode = $true         if (-not $IncrementalMode) { $IncrementalMode = $true }     }          # Ketika -StreamingMode diaktifkan, proses file dalam bagian yang hanya menyimpan penghitung dalam memori.# Data tingkat perangkat ditulis ke file JSON per-chunk untuk pemuatan sesuai permintaan di dasbor.# Penggunaan memori: ~1,5 GB terlepas dari ukuran kumpulan data (vs 10-20 GB tanpa streaming).if ($StreamingMode) {         Write-Host "MODE STREAMING diaktifkan - pemrosesan yang efisien memori" -ForegroundColor Green         $streamSw = [System.Diagnostics.Stopwatch]::StartNew()         # INCREMENTAL CHECK: Jika tidak ada file yang berubah sejak terakhir dijalankan, lewati pemrosesan seluruhnya         if ($IncrementalMode -and -not $ForceFullRefresh) {             $stManifestDir = Join-Path $OutputPath ".cache"             $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json"             if (Test-Path $stManifestPath) {                 Write-Host "Memeriksa perubahan sejak streaming terakhir dijalankan..." -ForegroundColor Cyan                 $stOldManifest = Get-FileManifest -Path $stManifestPath                 if ($stOldManifest.Count -gt 0) {                     $stChanged = $false                     # Pemeriksaan cepat: jumlah file yang sama?                     if ($stOldManifest.Count -eq $totalFiles) {                         # Periksa 100 file TERBARU (diurutkan menurut LastWriteTime turun)                         # Jika ada file yang diubah, akan memiliki stempel waktu terbaru dan muncul terlebih dahulu                         $sampleSize = [matematika]::Min(100, $totalFiles)                         $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc -Turun | Select-Object -$sampleSize pertama                         foreach ($sf di $sampleFiles) {                             $sfKey = $sf. FullName.ToLowerInvariant()                             if (-not $stOldManifest.ContainsKey($sfKey)) {                                 $stChanged = $true                                 Istirahat                             }                             # Bandingkan cap waktu - singgahan mungkin DateTime atau string setelah roundtrip JSON                             $cachedLWT = $stOldManifest[$sfKey]. LastWriteTimeUtc                             $fileDT = $sf. LastWriteTimeUtc                             coba {                                 # Jika singgahan sudah DateTime (ConvertFrom-Json auto-converts), gunakan secara langsung                                 if ($cachedLWT -is [DateTime]) {                                     $cachedDT = $cachedLWT.ToUniversalTime()                                 } lain {                                     $cachedDT = [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime                                 }                                 if ([matematika]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) {                                     $stChanged = $true                                     Istirahat                                 }                             } tangkap {                                 $stChanged = $true                                 Istirahat                             }                         }                     } lain {                         $stChanged = $true                     }                     if (-not $stChanged) {                         # Periksa apakah ada file output                         $stSummaryExists = Get-ChildItem ($OutputPath Jalur Bergabung "SecureBoot_Summary_*.csv") -EA SilentlyContinue | Select-Object -1 Pertama                         $stDashExists = Get-ChildItem ($OutputPath Jalur Gabungan "SecureBoot_Dashboard_*.html") -EA SilentlyContinue | Select-Object -1 Pertama                         if ($stSummaryExists -and $stDashExists) {                             Write-Host " Tidak ada perubahan yang terdeteksi ($totalFiles file tidak berubah) - melewati pemrosesan" -ForegroundColor Green                             Write-Host " Dasbor terakhir: $($stDashExists.FullName)" -ForegroundColor White                             $cachedStats = Get-Content $stSummaryExists.FullName | ConvertFrom-Csv                             Write-Host " Perangkat: $($cachedStats.TotalDevices) | Diperbarui: $($cachedStats.Diperbarui) | Kesalahan: $($cachedStats.WithErrors)" -ForegroundColor Gray                             Write-Host " Selesai dalam $([matematika]:Round($streamSw.Elapsed.TotalSeconds, 1)s (tidak diperlukan pemrosesan)" -ForegroundColor Green                             $cachedStats pengembalian                         }                     } lain {                         # DELTA PATCH: Temukan persis file mana yang diubah                         Write-Host " Perubahan terdeteksi - mengidentifikasi file yang diubah..." -ForegroundColor Yellow                         $changedFiles = [System.Collections.ArrayList]::new()                         $newFiles = [System.Collections.ArrayList]::new()                         foreach ($jf di $jsonFiles) {                             $jfKey = $jf. FullName.ToLowerInvariant()                             if (-not $stOldManifest.ContainsKey($jfKey)) {                                 [void]$newFiles.Add($jf)                             } lain {                                 $cachedLWT = $stOldManifest[$jfKey]. LastWriteTimeUtc                                 $fileDT = $jf. LastWriteTimeUtc                                 coba {                                     $cachedDT = if ($cachedLWT -is [DateTime]) { $cachedLWT.ToUniversalTime() } else { [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime }                                     if ([matematika]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { [void]$changedFiles.Add($jf) }                                 } tangkap { [void]$changedFiles.Add($jf) }                             }                         }                         $totalChanged = $changedFiles.Count + $newFiles.Count                         $changePct = [matematika]::Round(($totalChanged / $totalFiles) * 100, 1)                         Write-Host " Diubah: $($changedFiles.Count) | Baru: $($newFiles.Count) | Total: $totalChanged ($changePct%)" -ForegroundColor Yellow                         if ($totalChanged -gt 0 -and $changePct -lt 10) {                             # DELTA PATCH MODE: <10% berubah, patch data yang sudah ada                             Write-Host " Mode patch Delta ($changePct% < 10%) - patching file $totalChanged..." -ForegroundColor Green                             $dataDir = Join-Path $OutputPath "data"                             # Muat catatan perangkat yang diubah/baru                             $deltaDevices = @{}                             $allDeltaFiles = @($changedFiles) + @($newFiles)                             foreach ($df di $allDeltaFiles) {                                 coba {                                     $devData = Get-Content $df. FullName -Raw | ConvertFrom-Json                                     $dev = Normalize-DeviceRecord $devData                                     jika ($dev. HostName) { $deltaDevices[$dev. HostName] = $dev }                                 } tangkap { }                             }                             Write-Host " Loaded $($deltaDevices.Count) mengubah catatan perangkat" -ForegroundColor Gray                             # Untuk setiap kategori JSON: hapus entri lama untuk nama host yang diubah, tambahkan entri baru                             $categoryFiles = @("errors", "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 di $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) }                             kudeta ($cat di $categoryFiles) {                                 $catPath = Join-Path $dataDir "$cat.json"                                 if (Test-Path $catPath) {                                     coba {                                         $catData = Get-Content $catPath -Raw | ConvertFrom-Json                                         # Hapus entri lama untuk nama host yang diubah                                         $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. HostName) })                                         # Klasifikasi ulang setiap perangkat yang diubah ke dalam kategori                                         # (akan ditambahkan di bawah ini setelah klasifikasi)                                         $catData | ConvertTo-Json -Kedalaman 5 | Set-Content $catPath -Pengodean UTF8                                     } tangkap { }                                 }                             }                             # Klasifikasi setiap perangkat yang diubah dan tambahkan ke file kategori yang tepat                             foreach ($dev dalam $deltaDevices.Values) {                                 $slim = [dipesan]@{                                     HostName = $dev. HostName                                     WMI_Manufacturer = jika ($dev. PSObject.Properties['WMI_Manufacturer']) { $dev. WMI_Manufacturer } lagi { "" }                                     WMI_Model = jika ($dev. PSObject.Properties['WMI_Model']) { $dev. WMI_Model } lagi { "" }                                     BucketId = if ($dev. PSObject.Properties['BucketId']) { $dev. BucketId } else { "" }                                     ConfidenceLevel = if ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" }                                     IsUpdated = $dev. Diupdated                                     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 } else { $null }                                     SkipReasonKnownIssue = if ($dev. PSObject.Properties['SkipReasonKnownIssue']) { $dev. SkipReasonKnownIssue } else { $null }                                 }                                 $isUpd = $dev. IsUpdated -eq $true                                 $conf = jika ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" }                                 $hasErr = (-not [string]::IsNullOrEmpty($dev. UEFICA2023Error) dan $dev. UEFICA2023Error -ne "0" -dan $dev. UEFICA2023Error -ne "")                                 $tskDis = ($dev. SecureBootTaskEnabled -eq $false -atau $dev. SecureBootTaskStatus -eq 'Disabled' -or $dev. SecureBootTaskStatus -eq 'NotFound')                                 $tskNF = ($dev. SecureBootTaskStatus -eq 'NotFound')                                 $sbOn = ($dev. SecureBootEnabled -ne $false -and "$($dev. SecureBootEnabled)" -ne "False")                                 $e 1801 = jika ($dev. PSObject.Properties['Event1801Count']) { [int]$dev. Event1801Count } else { 0 }                                 $e 1808 = jika ($dev. PSObject.Properties['Event1808Count']) { [int]$dev. Event1808Count } else { 0 }                                 $e 1803 = jika ($dev. PSObject.Properties['Event1803Count']) { [int]$dev. Event1803Count } else { 0 }                                 $mKEK = ($e 1803 -gt 0 -atau $dev. MissingKEK -eq $true)                                 $hKI = ((-not [string]::IsNullOrEmpty($dev. SkipReasonKnownIssue)) -or (-not [string]::IsNullOrEmpty($dev. KnownIssueId)))                                 $rStat = jika ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } lagi { "" }                                 # Tambahkan ke file kategori yang cocok                                 $targets = @()                                 if ($isUpd) { $targets += "updated_devices" }                                 if ($hasErr) { $targets += "errors" }                                 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 di $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 -Kedalaman 5 | Set-Content $tgtPath -Pengodean UTF8                                     }                                 }                             }                             # Regenerasi CSV dari JSON yang ditambal                             Write-Host " Meregenerasi CSV dari data yang ditambal..." -ForegroundColor Gray                             $newTimestamp = Get-Date -Format "yyyyMMdd-HHmmss"                             foreach ($cat di $categoryFiles) {                                 $catJsonPath = Join-Path $dataDir "$cat.json"                                 $catCsvPath = Join-Path $OutputPath "SecureBoot_${cat}_$newTimestamp.csv"                                 if (Test-Path $catJsonPath) {                                     coba {                                         $catJsonData = Get-Content $catJsonPath -Raw | ConvertFrom-Json                                         if ($catJsonData.Count -gt 0) {                                             $catJsonData | Export-Csv -Path $catCsvPath -NoTypeInformation -Encoding UTF8                                         }                                     } tangkap { }                                 }                             }                             # Recount stats from the patched JSON files                             Write-Host " Menghitung ulang ringkasan dari data yang ditambal..." -Warna Latar Depan Abu-abu                             $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 di $categoryFiles) {                                 $catPath = Join-Path $dataDir "$cat.json"                                 $cnt = 0                                 if (Test-Path $catPath) { try { $cnt = (Get-Content $catPath -Raw | ConvertFrom-Json). Count } tangkapan { } }                                 sakelar ($cat) {                                     "updated_devices" { $pUpdated = $cnt }                                     "kesalahan" { $pErrors = $cnt }                                     "known_issues" { $pKI = $cnt }                                     "missing_kek" { $pKEK = $cnt }                                     "not_updated" { } # terhitung                                     "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). Menghitung                             $pTotal = $pUpdated + $pNotUpdated + $pSBOff                             Write-Host " Patch Delta lengkap: perangkat $totalChanged diperbarui" -ForegroundColor Green                             Write-Host " Total: $pTotal | Diperbarui: $pUpdated | NotUpdated: $pNotUpdated | Kesalahan: $pErrors" -ForegroundColor White                             # Perbarui manifes                             $stManifestDir = Join-Path $OutputPath ".cache"                             $stNewManifest = @{}                             foreach ($jf di $jsonFiles) {                                 $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{                                     LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o"); Ukuran = $jf. Panjang                                 }                             }                             Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath                             Write-Host " Selesai dalam $([matematika]:Round($streamSw.Elapsed.TotalSeconds, 1))s (delta patch - $totalChanged devices)" -ForegroundColor Green                             # Jatuh melalui ke streaming penuh reprocess untuk meregenerasi dasbor HTML                             # File data sudah ditambal, jadi ini memastikan dasbor tetap terkini                             Write-Host " Meregenerasi dasbor dari data yang ditambal..." -ForegroundColor Yellow                         } lain {                             Write-Host " $changePct% file berubah (>= 10%) - diperlukan pengolah ulang streaming penuh" -ForegroundColor Yellow                         }                     }                 }             }         }         # Buat subdirektori data untuk file JSON perangkat sesuai permintaan         $dataDir = Join-Path $OutputPath "data"         if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null }         # Deduplikasi melalui HashSet (O(1) per pencarian, ~50MB untuk nama host 600K)         $seenHostnames = [System.Collections.Generic.HashSet[string]]:new([System.StringComparer]::OrdinalIgnoreCase)         # Penghitung ringkasan ringan (menggantikan $allDevices + $uniqueDevices dalam memori)         $c = @{             Total = 0; SBEnabled = 0; SBOff = 0             Diperbarui = 0; HighConf = 0; UnderObs = 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         }         # Pelacakan wadah untuk AtRisk/SafeList (set ringan)         $stFailedBuckets = [System.Collections.Generic.HashSet[string]]::new()         $stSuccessBuckets = [System.Collections.Generic.HashSet[string]]::new()         $stAllBuckets = @{}         $stMfrCounts = @{}         $stErrorCodeCounts = @{}; $stErrorCodeSamples = @{}         $stKnownIssueCounts = @{}         # File data perangkat mode batch: akumulasi per-potong, siram pada batas-batas bagian         $stDeviceFiles = @("errors", "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 di $stDeviceFiles) {             $dfPath = Join-Path $dataDir "$dfName.json"             [System.IO.File]::WriteAllText($dfPath, "['n", [System.Text.Encoding]::UTF8)             $stDeviceFilePaths[$dfName] = $dfPath; $stDeviceFileCounts[$dfName] = 0         }         # Catatan perangkat ramping untuk output JSON (hanya bidang penting, ~200 byte vs ~2KB penuh)         fungsi Get-SlimDevice {             param($Dev)             return [ordered]@{                 HostName = $Dev.HostName                 WMI_Manufacturer = if ($Dev.PSObject.Properties['WMI_Manufacturer']) { $Dev.WMI_Manufacturer } else { "" }                 WMI_Model = if ($Dev.PSObject.Properties['WMI_Model']) { $Dev.WMI_Model } else { "" }                 BucketId = if ($Dev.PSObject.Properties['BucketId']) { $Dev.BucketId } else { "" }                 ConfidenceLevel = if ($Dev.PSObject.Properties['ConfidenceLevel']) { $Dev.ConfidenceLevel } else { "" }                 Diupdated = $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 } else { $null }                 SkipReasonKnownIssue = if ($Dev.PSObject.Properties['SkipReasonKnownIssue']) { $Dev.SkipReasonKnownIssue } else { $null }                 UEFICA2023Status = if ($Dev.PSObject.Properties['UEFICA2023Status']) { $Dev.UEFICA2023Status } else { $null }                 AvailableUpdatesPolicy = if ($Dev.PSObject.Properties['AvailableUpdatesPolicy']) { $Dev.AvailableUpdatesPolicy } else { $null }                 WinCSKeyApplied = if ($Dev.PSObject.Properties['WinCSKeyApplied']) { $Dev.WinCSKeyApplied } else { $null }             }         }         # Flush batch ke file JSON (mode tambahan)         fungsi 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 di $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)         }         # LOOP STREAMING UTAMA         $stChunkSize = if ($totalFiles -le 10000) { $totalFiles } else { 10000 }         $stTotalChunks = [matematika]::Plafon($totalFiles / $stChunkSize)         $stPeakMemMB = 0         if ($stTotalChunks -gt 1) {             Write-Host "Memproses file $totalFiles dalam bagian $stTotalChunks $stChunkSize (streaming, utas $ParallelThreads):" -ForegroundColor Cyan         } lain {             Write-Host "Memproses file $totalFiles (streaming, utas $ParallelThreads):" -ForegroundColor Sian         }         untuk ($ci = 0; $ci -lt $stTotalChunks; $ci++) {             $cStart = $ci * $stChunkSize             $cEnd = [matematika]::Min($cStart + $stChunkSize, $totalFiles) - 1             $cFiles = $jsonFiles[$cStart.. $cEnd]             if ($stTotalChunks -gt 1) {                 Write-Host " Chunk $($ci + 1)/$stTotalChunks ($($cFiles.Count) file): " -NoNewline -ForegroundColor Gray             } lain {                 Write-Host " Memuat file $($cFiles.Count): " -NoNewline -ForegroundColor Gray             }             $cSw = [System.Diagnostics.Stopwatch]::StartNew()             $rawDevices = Load-FilesParallel -Files $cFiles -Threads $ParallelThreads             # Daftar kumpulan per-bongkahan             $cBatches = @{}             foreach ($df dalam $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]::new() }             $cNew = 0; $cDupe = 0             foreach ($raw di $rawDevices) {                 if (-not $raw) { continue }                 $device = Normalize-DeviceRecord $raw                 $hostname = $device. HostName                 if (-not $hostname) { continue }                 if ($seenHostnames.Contains($hostname)) { $cDupe++; lanjutkan }                 [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 = jika ($device. PSObject.Properties['ConfidenceLevel'] -and $device. ConfidenceLevel) { "$($device. ConfidenceLevel)" } else { "" }                 $hasErr = (-not [string]::IsNullOrEmpty($device. UEFICA2023Error) -dan "$($device. UEFICA2023Error)" -ne "0" -dan "$($device. UEFICA2023Error)" -ne "")                 $tskDis = ($device. SecureBootTaskEnabled -eq $false -or "$($device. SecureBootTaskStatus)" -eq 'Disabled' -or "$($device. SecureBootTaskStatus)" -eq 'NotFound')                 $tskNF = ("$($device. SecureBootTaskStatus)" -eq 'NotFound')                 $bid = jika ($device. PSObject.Properties['BucketId'] -and $device. BucketId) { "$($device. BucketId)" } else { "" }                 $e 1808 = jika ($device. PSObject.Properties['Event1808Count']) { [int]$device. Event1808Count } else { 0 }                 $e 1801 = jika ($device. PSObject.Properties['Event1801Count']) { [int]$device. Event1801Count } else { 0 }                 $e 1803 = jika ($device. PSObject.Properties['Event1803Count']) { [int]$device. Event1803Count } else { 0 }                 $mKEK = ($e 1803 -gt 0 -atau $device. MissingKEK -eq $true -or "$($device. MissingKEK)" -eq "True")                 $hKI = ((-not [string]::IsNullOrEmpty($device. SkipReasonKnownIssue)) -or (-not [string]::IsNullOrEmpty($device. KnownIssueId)))                 $rStat = jika ($device. PSObject.Properties['RolloutStatus']) { $device. RolloutStatus } lagi { "" }                 $mfr = jika ($device. PSObject.Properties['WMI_Manufacturer'] -and -not [string]::IsNullOrEmpty($device. WMI_Manufacturer)) { $device. WMI_Manufacturer } else { "Unknown" }                 $bid = if (-not [string]::IsNullOrEmpty($bid)) { $bid } else { "" }                 # Bendera Tertunda Pembaruan Prakomputasi (kebijakan/WinCS diterapkan, status belum Diperbarui, SB AKTIF, tugas tidak dinonaktifkan)                 $uefiStatus = jika ($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'] -dan $device. WinCSKeyApplied -eq $true)                 $statusPending = ([string]::IsNullOrEmpty($uefiStatus) -atau $uefiStatus -eq 'NotStarted' -or $uefiStatus -eq 'InProgress')                 $isUpdatePending = (($hasPolicy -atau $hasWinCS) -dan $statusPending -dan -bukan $isUpd -dan $sbOn -dan -bukan $tskDis)                 if ($isUpd) {                     $c.Updated++; [void]$stSuccessBuckets.Add($bid); $cBatches["updated_devices"]. Add((Get-SlimDevice $device))                     # Lacak Perangkat yang diperbarui yang memerlukan boot ulang (UEFICA2023Status=Diperbarui tapi Event1808=0)                     if ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) }                 }                 elseif (-not $sbOn) {                     # SecureBoot OFF — di luar lingkup, jangan mengklasifikasikan dengan percaya diri                 }                 lainnya {                     if ($isUpdatePending) { } # Dihitung secara terpisah dalam Pembaruan Tertunda — sama eksklusifnya untuk bagan pai                     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++ }                 if ($hasErr) {                     $c.WithErrors++; [void]$stFailedBuckets.Add($bid); $cBatches["errors"]. Add((Get-SlimDevice $device))                     $ec = $device. UEFICA2023Error                     if (-not $stErrorCodeCounts.ContainsKey($ec)) { $stErrorCodeCounts[$ec] = 0; $stErrorCodeSamples[$ec] = @() }                     $stErrorCodeCounts[$ec]++                     jika ($stErrorCodeSamples[$ec]. Hitung -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 } else { $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 -and $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++ }                 # Pembaruan Tertunda: kebijakan atau WinCS diterapkan, status tertunda, SB ON, tugas tidak dinonaktifkan                 if ($isUpdatePending) {                     $c.UpdatePending++; $cBatches["update_pending"]. Add((Get-SlimDevice $device))                 }                 if (-not $isUpd -and $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) }                 # Di bawah Perangkat observasi (terpisah dari Tindakan Diperlukan)                 if (-not $isUpd -and (Test-ConfidenceLevel $conf 'UnderObservation')) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) }                 # Tindakan Diperlukan: tidak diperbarui, SB AKTIF, tidak cocok dengan kategori kepercayaan lainnya, bukan Pembaruan Tertunda                 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; Diperbarui=0; UpdatePending=0; HighConf=0; UnderObs=0; ActionReq=0; TempPaused=0; NotSupported=0; SBOff=0; WithErrors=0 } }                 $stMfrCounts[$mfr]. Total++                 jika ($isUpd) { $stMfrCounts[$mfr]. Diperbarui++ }                 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]. UnderObs++ }                 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++ }                 # Lacak semua perangkat menurut wadah (termasuk BucketId kosong)                 $bucketKey = if ($bid -and $bid -ne "") { $bid } else { "(empty)" }                 if (-not $stAllBuckets.ContainsKey($bucketKey)) {                     $stAllBuckets[$bucketKey] = @{ Count=0; Diperbarui=0; Manufacturer=$mfr; Model=""; BIOS="" }                     jika ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. Model = $device. WMI_Model }                     jika ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription }                 }                 $stAllBuckets[$bucketKey]. Count++                 jika ($isUpd) { $stAllBuckets[$bucketKey]. Diperbarui++ }             }             # Flush kumpulan ke disk             foreach ($df di $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] }             $rawDevices = $null; $cBatches = $null; [System.GC]::Collect()             $cSw.Stop()             $cTime = [Matematika]::Round($cSw.Elapsed.TotalSeconds, 1)             $cRem = $stTotalChunks - $ci - 1             $cEta = jika ($cRem -gt 0) { " | ETA: ~$([Matematika]:Round($cRem * $cSw.Elapsed.TotalSeconds / 60, 1)) min" } else { "" }             $cMem = [matematika]:Round([System.GC]::GetTotalMemory($false) / 1MB, 0)             if ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem }             Write-Host " +$cNew baru, $cDupe dupes, ${cTime}s | Mem: ${cMem}MB$cEta" -ForegroundColor Green         }         # Menyelesaikan array JSON         foreach ($dfName di $stDeviceFiles) {             [System.IO.File]::AppendAllText($stDeviceFilePaths[$dfName], "'n]", [System.Text.Encoding]::UTF8)             Write-Host " $dfName.json: perangkat $($stDeviceFileCounts[$dfName]) -ForegroundColor DarkGray         }         # Statistik turunan komputasi         $stAtRisk = 0; $stSafeList = 0         foreach ($bid di $stAllBuckets.Keys) {             $b = $stAllBuckets[$bid]; $nu = $b.Count - $b.Updated             if ($stFailedBuckets.Contains($bid)) { $stAtRisk += $nu }             elseif ($stSuccessBuckets.Contains($bid)) { $stSafeList += $nu }         }         $stAtRisk = [matematika]::Maks(0, $stAtRisk - $c.WithErrors)         # NotUptodate = hitung dari kumpulan not_updated (perangkat dengan SB AKTIF dan tidak diperbarui)         $stNotUptodate = $stDeviceFileCounts["not_updated"]         $stats = [dipesan]@{             ReportGeneratedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss")             TotalDevices = $c.Total; SecureBootEnabled = $c.SBEnabled; SecureBootOFF = $c.SBOff             Diperbarui = $c.Diperbarui; HighConfidence = $c.HighConf; UnderObservation = $c.UnderObs             ActionRequired = $c.ActionReq; SementaraPaused = $c.TempPaused; NotSupported = $c.notSupported             NoConfidenceData = $c.NoConfData; TaskDisabled = $c.TaskDisabled; TaskNotFound = $c.TaskNotFound             TaskDisabledNotUpdated = $c.TaskDisabledNotUpdated             CertificatesUpdated = $c.Updated; NotUptodate = $stNotUptodate; FullyUpdated = $c.Updated             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 }             PersenHighConfidence = if ($c.Total -gt 0) { [math]::Round(($c.HighConf/$c.Total)*100,1) } else { 0 }             PercentCertUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 }             PercentActionRequired = if ($c.Total -gt 0) { [math]::Round(($c.ActionReq/$c.Total)*100,1) } else { 0 }             PercentNotUptodate = if ($c.Total -gt 0) { [math]::Round($stNotUptodate/$c.Total*100,1) } else { 0 }             PersenFullyUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 }             UniqueBuckets = $stAllBuckets.Count; PeakMemoryMB = $stPeakMemMB; ProcessingMode = "Streaming"         }         # Tulis CSV         [PSCustomObject]$stats | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_Summary_$timestamp.csv") -NoTypeInformation -Encoding UTF8         $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Turun | ForEach-Object {             [PSCustomObject]@{ Manufacturer=$_. Kunci; Count=$_. Value.Total; Diperbarui=$_. Value.Updated; HighConfidence=$_. Value.HighConf; ActionRequired=$_. Value.ActionReq }         } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_ByManufacturer_$timestamp.csv") -NoTypeInformation -Encoding UTF8         $stErrorCodeCounts.GetEnumerator() | Nilai Sort-Object -Turun | ForEach-Object {             [PSCustomObject]@{ ErrorCode=$_. Kunci; Count=$_. Nilai; SampleDevices=($stErrorCodeSamples[$_. Kunci] -join ", ") }         } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_ErrorCodes_$timestamp.csv") -NoTypeInformation -Encoding UTF8         $stAllBuckets.GetEnumerator() | Sort-Object { $_. Value.Count } -Turun | ForEach-Object {             [PSCustomObject]@{ BucketId=$_. Kunci; Count=$_. Value.Count; Diperbarui=$_. Value.Updated; NotUpdated=$_. Value.Count-$_. Value.Updated; Produsen=$_. Value.Manufacturer }         } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_UniqueBuckets_$timestamp.csv") -NoTypeInformation -Encoding UTF8         # Hasilkan CSV yang kompatibel dengan orkestrator (nama file yang diharapkan untuk Start-SecureBootRolloutOrchestrator.ps1)         $notUpdatedJsonPath = Join-Path $dataDir "not_updated.json"         if (Test-Path $notUpdatedJsonPath) {             coba {                 $nuData = Get-Content $notUpdatedJsonPath -Raw | ConvertFrom-Json                 if ($nuData.Count -gt 0) {                     # NotUptodate CSV - orkestrator mencari *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) perangkat)" -ForegroundColor Gray                 }             } tangkap { }         }         # Tulis data JSON untuk dasbor         $stats | ConvertTo-Json -Kedalaman 3 | Set-Content ($dataDir Jalur Bergabung "summary.json") -Pengodean UTF8         # HISTORIS PELACAKAN: Simpan titik data untuk bagan tren         # Gunakan lokasi singgahan yang stabil sehingga data tren tetap ada di seluruh folder agregasi bertingkat waktu.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   # Jika OutputPath terlihat seperti "...\Aggregation_yyyyMMdd_HHmmss", cache akan masuk ke folder induk.# Jika tidak, cache masuk ke dalam OutputPath itu sendiri.$parentDir = Split-Path $OutputPath -Parent         $leafName = Split-Path $OutputPath -Leaf         if ($leafName -match '^Aggregation_\d{8}' -or $leafName -eq 'Aggregation_Current') {             # Orchestrator-create timestamped folder — use parent for stable cache             $historyPath = Join-Path $parentDir ".cache\trend_history.json"         } lain {             $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) {             coba { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } tangkapan { $historyData = @() }         }         # Periksa juga di dalam OutputPath\.cache\ (lokasi warisan dari versi yang lebih lama)         # Gabungkan titik data apa pun yang belum ada dalam riwayat utama         if ($leafName -eq 'Aggregation_Current' -or $leafName -match '^Aggregation_\d{8}') {             $innerHistoryPath = Join-Path $OutputPath ".cache\trend_history.json"             if ((Test-Path $innerHistoryPath) -and $innerHistoryPath -ne $historyPath) {                 coba {                     $innerData = @(Get-Content $innerHistoryPath -Raw | ConvertFrom-Json)                     $existingDates = @($historyData | ForEach-Object { $_. Tanggal })                     foreach ($entry di $innerData) {                         jika ($entry. Tanggal dan $entry. Tanggal -notin $existingDates) {                             $historyData += $entry                         }                     }                     if ($innerData.Count -gt 0) {                         Write-Host " Titik data $($innerData.Count) gabungan dari cache dalam" -ForegroundColor DarkGray                     }                 } tangkap { }             }         }

# BOOTSTRAP: Jika riwayat tren kosong/jarang, rekonstruksi dari data riwayat         if ($historyData.Count -lt 2 -and ($leafName -match '^Aggregation_\d{8}' -or $leafName -eq 'Aggregation_Current')) {             Write-Host " Riwayat tren bootstrapping dari data historis..." -ForegroundColor Yellow             $dailyData = @{}                          # Sumber 1: CSV ringkasan di dalam folder saat ini (Aggregation_Current menyimpan semua CSV Ringkasan)             $localSummaries = Get-ChildItem $OutputPath -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Nama Sort-Object             foreach ($summCsv di $localSummaries) {                 coba {                     $summ = Import-Csv $summCsv.FullName | Select-Object -1 Pertama                     jika ($summ. TotalDevices -dan [int]$summ. TotalDevices -gt 0 -dan $summ. ReportGeneratedAt) {                         $dateStr = ([datetime]$summ. ReportGeneratedAt). ToString("yyyy-MM-dd")                         $updated = jika ($summ. Diperbarui) { [int]$summ. Diperbarui } lagi { 0 }                         $notUpd = jika ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated }                         $dailyData[$dateStr] = [PSCustomObject]@{                             Tanggal = $dateStr; Total = [int]$summ. TotalDevices; Diperbarui = $updated; NotUpdated = $notUpd                             NeedsReboot = 0; Kesalahan = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 }                         }                     }                 } tangkap { }             }                          # Sumber 2: Folder lama Aggregation_* (warisan, jika masih ada)             $aggFolders = Get-ChildItem $parentDir -Directory -Filter "Aggregation_*" -EA SilentlyContinue |                 Where-Object { $_. Nama -match '^Aggregation_\d{8}' } |                 Nama Sort-Object             foreach ($folder di $aggFolders) {                 $summCsv = Get-ChildItem $folder. FullName -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Select-Object -1 Pertama                 if ($summCsv) {                     coba {                         $summ = Import-Csv $summCsv.FullName | Select-Object -1 Pertama                         jika ($summ. TotalDevices -dan [int]$summ. TotalDevices -gt 0) {                             $dateStr = $folder. Nama -replace '^Aggregation_(\d{4})(\d{2})(\d{2})_.*', '$1-$2-$3'                             $updated = jika ($summ. Diperbarui) { [int]$summ. Diperbarui } lagi { 0 }                             $notUpd = jika ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated }                             $dailyData[$dateStr] = [PSCustomObject]@{                                 Tanggal = $dateStr; Total = [int]$summ. TotalDevices; Diperbarui = $updated; NotUpdated = $notUpd                                 NeedsReboot = 0; Kesalahan = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 }                             }                         }                     } tangkap { }                 }             }                          # Sumber 3: RolloutState.json WaveHistory (memiliki stempel waktu per gelombang dari hari 1)             # Ini menyediakan titik data garis dasar bahkan ketika tidak ada folder agregasi lama             $rolloutStatePaths = @(                 (Join-Path $parentDir "RolloutState\RolloutState.json"),                 (Join-Path $OutputPath "RolloutState\RolloutState.json")             )             foreach ($rsPath di $rolloutStatePaths) {                 if (Test-Path $rsPath) {                     coba {                         $rsData = Get-Content $rsPath -Raw | ConvertFrom-Json                         if ($rsData.WaveHistory) {                             # Gunakan tanggal mulai gelombang sebagai titik data tren                             # Hitung perangkat kumulatif yang ditargetkan pada setiap gelombang                             $cumulativeTargeted = 0                             foreach ($wave di $rsData.WaveHistory) {                                 jika ($wave. StartedAt -and $wave. DeviceCount) {                                     $waveDate = ([datetime]$wave. StartedAt). ToString("yyyy-MM-dd")                                     $cumulativeTargeted += [int]$wave. DeviceCount                                     if (-not $dailyData.ContainsKey($waveDate)) {                                         # Perkiraan: pada waktu mulai gelombang, hanya perangkat dari gelombang sebelumnya yang diperbarui                                         $dailyData[$waveDate] = [PSCustomObject]@{                                             Tanggal = $waveDate; Total = $c.Total; Diperbarui = [matematika]::Max(0, $cumulativeTargeted - [int]$wave. DeviceCount)                                             NotUpdated = $c.Total - [math]::Max(0, $cumulativeTargeted - [int]$wave. DeviceCount)                                             NeedsReboot = 0; Kesalahan = 0; ActionRequired = 0                                         }                                     }                                 }                             }                         }                     } tangkap { }                     break # Gunakan pertama kali ditemukan                 }             }

if ($dailyData.Count -gt 0) {                 $historyData = @($dailyData.GetEnumerator() | Kunci Sort-Object | ForEach-Object { $_. Nilai })                 Write-Host " Titik data $($historyData.Count) bootstrapped dari ringkasan historis" -ForegroundColor Green             }         }

# Tambahkan titik data saat ini (deduplikasi menurut hari - tetap terbaru per hari)         $todayKey = (Get-Date). ToString("yyyy-MM-dd")         $existingToday = $historyData | Where-Object { "$($_. Date)" -like "$todayKey*" }         if ($existingToday) {             # Ganti entri hari ini             $historyData = @($historyData | Where-Object { "$($_. Date)" -notlike "$todayKey*" })         }         $historyData += [PSCustomObject]@{             Tanggal = $todayKey             Total = $c.Total             Diperbarui = $c.Diperbarui             NotUpdated = $stNotUptodate             NeedsReboot = $c.NeedsReboot             Kesalahan = $c.WithErrors             ActionRequired = $c.ActionReq         }         # Hapus titik data yang buruk (total 0) dan pertahankan 90 terakhir         $historyData = @($historyData | Where-Object { [int]$_. Total -gt 0 })         # Tanpa tutup — data tren ~100 byte/entry, setahun penuh = ~36 KB         $historyData | ConvertTo-Json -Kedalaman 3 | Set-Content $historyPath -Pengodean UTF8         Write-Host " Riwayat tren: titik data $($historyData.Count) " -ForegroundColor DarkGray                  # Susun data bagan tren untuk HTML         $trendLabels = ($historyData | ForEach-Object { "'$($_. Tanggal)'" }) -join ","         $trendUpdated = ($historyData | ForEach-Object { $_. Diperbarui }) -join ","         $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) -join ","         $trendTotal = ($historyData | ForEach-Object { $_. Total }) -join ","         # Proyeksi: memperluas garis tren menggunakan penggambatan eksponensial (2,4,8,16...)         # Memperoleh ukuran gelombang dan periode pengamatan dari data riwayat tren aktual.# - Ukuran gelombang = peningkatan periode tunggal terbesar terlihat dalam sejarah (gelombang terbaru yang digunakan)         # - Hari observasi = rata-rata hari kalender antara titik data tren (seberapa sering kita menjalankan)         # Lalu gandakan ukuran gelombang setiap periode, sesuai dengan strategi pertumbuhan 2x orkestrator.$projLabels = ""; $projUpdated = ""; $projNotUpdated = ""; $hasProjection = $false         if ($historyData.Count -ge 2) {             $lastUpdated = $c.Diperbarui             $remaining = $stNotUptodate # Hanya perangkat yang tidak diperbarui SB-ON (tidak termasuk SecureBoot NONAKTIF)             $projDates = @(); $projValues = @(); $projNotUpdValues = @()             $projDate = Get-Date

# Derive wave size and observation period from trend history             $increments = @()             $dayGaps = @()             for ($hi = 1; $hi -lt $historyData.Count; $hi++) {                 $inc = $historyData[$hi]. Diperbarui - $historyData[$hi-1]. Diperbarui                 if ($inc -gt 0) { $increments += $inc }                 coba {                     $d 1 = [datetime]::P arse($historyData[$hi-1]. Tanggal)                     $d 2 = [datetime]::P arse($historyData[$hi]. Tanggal)                     $gap = ($d 2 - $d 1). TotalDays                     if ($gap -gt 0) { $dayGaps += $gap }                 } tangkap {}             }             # Ukuran gelombang = penambahan positif terbaru (gelombang saat ini), mundur ke rata-rata, minimum 2             $waveSize = jika ($increments. Hitung -gt 0) {                 [matematika]::Max(2, $increments[-1])             } lagi { 2 }             # Periode observasi = kesenjangan rata-rata antara titik data (hari kalender per gelombang), minimal 1             $waveDays = if ($dayGaps.Count -gt 0) {                 [matematika]::Maks(1, [matematika]::Round(($dayGaps | Measure-Object -Average). Rata-rata, 0))             } lagi { 1 }

            Write-Host " Proyeksi: waveSize=$waveSize (dari penambahan terakhir), waveDays=$waveDays (kesenjangan avg dari sejarah)" -ForegroundColor DarkGray

$dayCounter = 0             # Proyek hingga semua perangkat diperbarui atau maksimal 365 hari             untuk ($pi = 1; $pi -le 365; $pi++) {                 $projDate = $projDate.AddDays(1)                 $dayCounter++                 # Pada setiap batas periode observasi, sebarkan gelombang kemudian ganda                 if ($dayCounter -ge $waveDays) {                     $devicesThisWave = [matematika]::Min($waveSize, $remaining)                     $lastUpdated += $devicesThisWave                     $remaining -= $devicesThisWave                     if ($lastUpdated -gt ($c.Updated + $stNotUptodate)) { $lastUpdated = $c.Diperbarui + $stNotUptodate; $remaining = 0 }                     # Ukuran gelombang ganda untuk periode berikutnya (strategi orkestrator 2x)                     $waveSize = $waveSize * 2                     $dayCounter = 0                 }                 $projDates += "'$($projDate.ToString("yyyy-MM-dd"))'"                 $projValues += $lastUpdated                 $projNotUpdValues += [matematika]:Maks(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 " Proyeksi: membutuhkan setidaknya 2 titik data tren untuk mendapatkan pengaturan waktu gelombang" -ForegroundColor DarkGray         }         # Susun string data bagan gabungan untuk string di sini         $allChartLabels = if ($hasProjection) { "$trendLabels,$projLabels" } else { $trendLabels }         $projDataJS = if ($hasProjection) { $projUpdated } else { "" }         $projNotUpdJS = if ($hasProjection) { $projNotUpdated } else { "" }         $histCount = ($historyData | Objek Pengukuran). Menghitung         $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Turun | ForEach-Object {             @{ name=$_. Kunci; total=$_. Value.Total; diperbarui=$_. Value.Updated; highConf=$_. Value.HighConf; actionReq=$_. Value.ActionReq }         } | ConvertTo-Json -Kedalaman 3 | Set-Content ($dataDir Jalur Bergabung "manufacturers.json") -Pengodean UTF8         # Konversi file data JSON ke CSV untuk unduhan Excel yang dapat dibaca manusia         Write-Host "Mengonversi data perangkat ke CSV untuk unduhan Excel..." -ForegroundColor Gray         foreach ($dfName di $stDeviceFiles) {             $jsonFile = Join-Path $dataDir "$dfName.json"             $csvFile = Join-Path $OutputPath "SecureBoot_${dfName}_$timestamp.csv"             if (Test-Path $jsonFile) {                 coba {                     $jsonData = Get-Content $jsonFile -Raw | ConvertFrom-Json                     if ($jsonData.Count -gt 0) {                         # Sertakan kolom tambahan untuk CSV update_pending                         $selectProps = if ($dfName -eq "update_pending") {                             @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Status', 'UEFICA2023Error', 'AvailableUpdatesPolicy', 'WinCSKeyApplied', 'SecureBootTaskStatus')                         } lain {                             @('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) baris -> CSV" -ForegroundColor DarkGray                     }                 } menangkap { Write-Host " $dfName - dilewati" -ForegroundColor DarkYellow }             }         }         # Hasilkan dasbor HTML mandiri         $htmlPath = Join-Path $OutputPath "SecureBoot_Dashboard_$timestamp.html"         Write-Host "Menghasilkan dasbor HTML mandiri..." -ForegroundColor Yellow         # VELOCITY PROJECTION: Menghitung dari riwayat pemindaian atau ringkasan sebelumnya         $stDeadline = [datetime]"2026-06-24" # Sert KEK kedaluwarsa         $stDaysToDeadline = [matematika]::Max(0, ($stDeadline - (Get-Date)). Hari)         $stDevicesPerDay = 0         $stProjectedDate = $null         $stVelocitySource = "N/A"         $stWorkingDays = 0         $stCalendarDays = 0         # Cobalah riwayat tren terlebih dahulu (ringan, sudah dipertahankan oleh agregator — menggantikan ScanHistory.json yang membengkak)         if ($historyData.Count -ge 2) {             $validHistory = @($historyData | Where-Object { [int]$_. Total -gt 0 -and [int]$_. Diperbarui -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). TotalDays                 if ($daysDiff -gt 0) {                     $updDiff = [int]$curr. Diperbarui - [int]$prev. Diperbarui                     if ($updDiff -gt 0) {                         $stDevicesPerDay = [matematika]::Round($updDiff / $daysDiff, 0)                         $stVelocitySource = "TrendHistory"                     }                 }             }         }         # Coba ringkasan peluncuran orkestrator (memiliki kecepatan terkomputasi sebelumnya)         if ($stVelocitySource -eq "N/A" -and $RolloutSummaryPath -and (Test-Path $RolloutSummaryPath)) {             coba {                 $rolloutSummary = Get-Content $RolloutSummaryPath -Raw | ConvertFrom-Json                 if ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) {                     $stDevicesPerDay = [matematika]::Round([double]$rolloutSummary.DevicesPerDay, 1)                     $stVelocitySource = "Orkestrator"                     if ($rolloutSummary.ProjectedCompletionDate) {                         $stProjectedDate = $rolloutSummary.ProjectedCompletionDate                     }                     if ($rolloutSummary.WorkingDaysRemaining) { $stWorkingDays = [int]$rolloutSummary.WorkingDaysRemaining }                     if ($rolloutSummary.CalendarDaysRemaining) { $stCalendarDays = [int]$rolloutSummary.CalendarDaysRemaining }                 }             } tangkap { }         }         # Fallback: coba CSV ringkasan sebelumnya (cari folder saat ini AND folder agregasi induk/saudara)         if ($stVelocitySource -eq "N/A") {             $searchPaths = @(                 ($OutputPath Jalur Bergabung "SecureBoot_Summary_*.csv")             )             # Juga cari folder agregasi saudara (orkestrator membuat folder baru setiap jalankan)             $parentPath = Split-Path $OutputPath -Parent             if ($parentPath) {                 $searchPaths += ($parentPath Jalur Bergabung "Aggregation_*\SecureBoot_Summary_*.csv")                 $searchPaths += ($parentPath Jalur Gabungan "SecureBoot_Summary_*.csv")             }             $prevSummary = $searchPaths | ForEach-Object { Get-ChildItem $_ -EA SilentlyContinue } | Sort-Object LastWriteTime -Turun | Select-Object -1 Pertama             if ($prevSummary) {                 coba {                     $prevStats = Get-Content $prevSummary.FullName | ConvertFrom-Csv                     $prevDate = [datetime]$prevStats.ReportGeneratedAt                     $daysSinceLast = ((Get-Date) - $prevDate). TotalDays                     if ($daysSinceLast -gt 0,01) {                         $prevUpdated = [int]$prevStats.Diperbarui                         $updDelta = $c.Diperbarui - $prevUpdated                         if ($updDelta -gt 0) {                             $stDevicesPerDay = [matematika]::Round($updDelta / $daysSinceLast, 0)                             $stVelocitySource = "PreviousReport"                         }                     }                 } tangkap { }             }         }         # Fallback: menghitung kecepatan dari rentang riwayat tren penuh (titik data pertama vs terbaru)         if ($stVelocitySource -eq "N/A" -and $historyData.Count -ge 2) {             $validHistory = @($historyData | Where-Object { [int]$_. Total -gt 0 -and [int]$_. Diperbarui -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). TotalDays                 if ($daysDiff -gt 0) {                     $updDiff = [int]$last. Diperbarui - [int]$first. Diperbarui                     if ($updDiff -gt 0) {                         $stDevicesPerDay = [matematika]::Round($updDiff / $daysDiff, 1)                         $stVelocitySource = "TrendHistory"                     }                 }             }         }         # Menghitung proyeksi menggunakan penggambatan eksponensial (konsisten dengan bagan tren)         # Gunakan kembali data proyeksi yang sudah dihitung untuk bagan jika tersedia         if ($hasProjection -and $projDates.Count -gt 0) {             # Gunakan tanggal proyeksi terakhir (saat semua perangkat diperbarui)             $lastProjDateStr = $projDates[-1] -replace "'", ""             $stProjectedDate = ([datetime]::P arse($lastProjDateStr)). ToString("MMM dd, yyyy")             $stCalendarDays = ([datetime]::P arse($lastProjDateStr) - (Get-Date)). Hari             $stWorkingDays = 0             $d = Get-Date             for ($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 -and $stNotUptodate -gt 0) {             # Fallback: proyeksi linear jika tidak ada data eksponensial yang tersedia             $daysNeeded = [matematika]::Plafon($stNotUptodate / $stDevicesPerDay)             $stProjectedDate = (Get-Date). AddDays($daysNeeded). ToString("MMM dd, yyyy")             $stWorkingDays = 0; $stCalendarDays = $daysNeeded             $d = Get-Date             for ($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 = jika ($stDevicesPerDay -gt 0) {             "<><>& kuat#128640; Perangkat/Hari:</strong> $($stDevicesPerDay.ToString('N0')) (sumber: $stVelocitySource)</div>" +             "<><>& kuat#128197; Penyelesaian Yang Diproyeksikan:</strong> $stProjectedDate" +             $(if ($stProjectedDate -and [datetime]::P arse($stProjectedDate) -gt $stDeadline) { " <span style='color:#dc3545; font-weight:>&#9888; PAST DEADLINE</span>" } else { " <span style='color:#28a745'>&#10003; Sebelum tenggat waktu</span>" }) +             "</div>" +             "<><>&#128336 yang kuat; Hari Kerja:</strong> $stWorkingDays | <kuat>Calendar Hari:</strong> $stCalendarDays</div>" +             "<div style='font-size:.8em; color:#888'>Deadline: Jun 24, 2026 (Kedaluwarsa sertifikat KEK) | Hari tersisa: $stDaysToDeadline</div>"         } lain {             "<div style='padding:8px; latar belakang:#fff3cd; radius batas:4px; border-left:3px solid #ffc107'>" +             "<>&#128197 yang kuat; Penyelesaian yang Diproyeksikan:</strong> Data yang tidak mencukupi untuk penghitungan kecepatan.                                                                                  " +             "Jalankan agregasi setidaknya dua kali dengan perubahan data untuk menetapkan rate.<br/>" +             "<tenggat waktu>yang kuat:</strong> 24 Jun 2026 (Sertifikat KEK kedaluwarsa) | <hari>yang kuat tersisa:</strong> $stDaysToDeadline</div>"         }                  # Penghitungan mundur kedaluwarsa # Cert         $certToday = Get-Date         $certKekExpiry = [datetime]"2026-06-24"         $certUefiExpiry = [datetime]"2026-06-27"         $certPcaExpiry = [datetime]"2026-10-19"         $daysToKek = [matematika]::Maks(0, ($certKekExpiry - $certToday). Hari)         $daysToUefi = [matematika]::Maks(0, ($certUefiExpiry - $certToday). Hari)         $daysToPca = [matematika]::Maks(0, ($certPcaExpiry - $certToday). Hari)         $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } else { '#28a745' }                  # Helper: Membaca rekaman dari JSON, ringkasan wadah build + baris perangkat N pertama         $maxInlineRows = 200         fungsi Build-InlineTable {             param([string]$JsonPath, [int]$MaxRows = 200, [string]$CsvFileName = "")             $bucketSummary = ""             $deviceRows = ""             $totalCount = 0             if (Test-Path $JsonPath) {                 coba {                     $data = Get-Content $JsonPath -Raw | ConvertFrom-Json                     $totalCount = $data. Menghitung                                          # BUCKET SUMMARY: Mengelompokkan menurut BucketId, memperlihatkan hitungan per wadah dengan Diperbarui dari statistik bucket global                     if ($totalCount -gt 0) {                         $buckets = $data | Group-Object BucketId | Sort-Object Count -Descending                         $bucketSummary = "><2 h3 style='font-size:.95em; warna:#333; margin:10px 0 5px'><3 Menurut Bucket Perangkat Keras ($($buckets. Hitung) wadah)><4 /h3>"                         $bucketSummary += "><6 div style='max-height:300px; overflow-y:auto; margin-bottom:tabel ><15px><ad><tr><th><5 BucketID><6 /th><th style='text-align:right'>Total</th><th style='text-align:right; color:#28a745'>Diperbarui</th><th style='text-align:right; color:#dc3545'>Tidak Diperbarui</th><th><1 Produsen><2 /th></tr></thead><tbody>"                         kudeta ($b di $buckets) {                             $bid = if ($b.Name) { $b.Name } else { "(empty)" }                             $mfr = ($b.Group | Select-Object -1 Pertama). WMI_Manufacturer                             # Dapatkan hitungan yang diperbarui dari statistik bucket global (semua perangkat dalam wadah ini di seluruh kumpulan data)                             $lookupKey = $bid                             $globalBucket = if ($stAllBuckets.ContainsKey($lookupKey)) { $stAllBuckets[$lookupKey] } else { $null }                             $bUpdatedGlobal = if ($globalBucket) { $globalBucket.Updated } else { 0 }                             $bTotalGlobal = if ($globalBucket) { $globalBucket.Count } else { $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; warna:#28a745; font-weight:bold'>$bUpdatedGlobal><2 /td><td style='text-align:right; warna:#dc3545; font-weight:bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n"                         }                         $bucketSummary += "</tbody></table></div>"                     }                                          # DETAIL PERANGKAT: Baris N pertama sebagai daftar datar                     $slice = $data | Select-Object -$MaxRows pertama                     foreach ($d di $slice) {                         $conf = $d.ConfidenceLevel                         $confBadge = if ($conf -match "High") { '<span class="lencana-success">High Conf><2 /span>' }                                      elseif ($conf -match "Not Sup") { '<span class="lencana badge-danger">Tidak Didukung><6 /span>' }                                      elseif ($conf -match "Under") { '<span class="badge badge-info">Under Obs><0 /span>' }                                      elseif ($conf -match "Jeda") { '<span class="badge badge-warning">Jeda><4 /span>' }                                      else { '<span class="lencana-warning">Tindakan Req><8 /span>' }                         $statusBadge = if ($d.IsUpdated) { '><00 span class="lencana-sukses"><01 Memperbarui</span>' }                                        elseif ($d.UEFICA2023Error) { '><04 span class="lencana-bahaya"><05 Kesalahan</span>' }                                        else { '><08 span class="lencana-warning"><09 Tertunda><0 /span>' }                         $deviceRows += "><12 tr><td><5 $($d.HostName)><16 /td><td><9 $($d.WMI_Manufacturer)) ><20 /td><td><3 $($d.WMI_Model)><24 /td><td><7 $confBadge><8 /td><td><1 $statusBadge><2 /td td><td><5 $(if($d.UEFICA2023Error){$d.UEFICA2023Error}else{'-'})><36 td><td style='font-size:.75em'><39 $($d.BucketId)><40 /td></tr><3 'n"                     }                 } tangkap { }             }             if ($totalCount -eq 0) {                 return "><44 div style='padding:20px; warna:#888; gaya font:><45 Miring Tidak ada perangkat dalam category.><46 /div>"             }             $showing = [matematika]::Min($MaxRows, $totalCount)             $header = "><48 div style='margin:5px 0; font-size:.85em; color:#666'><49 Total: perangkat $($totalCount.ToString("N0")"             if ($CsvFileName) { $header += " | ><50 href='$CsvFileName' style='color:#1a237e; font-weight:bold'>&#128196; Unduh CSV Lengkap untuk Excel><3 /a>" }             $header += "><55 /div>"             $deviceHeader = "><57 h3 style='font-size:.95em; warna:#333; margin:Detail Perangkat ><58 10px 0 5px (memperlihatkan $showing pertama)><59 /h3>"             $deviceTable = "><61 div style='max-height:500px; overflow-y:tabel ><otomatis><><tr><th><0 HostName><1 /th><th><4 Produsen><5 /th><th model><8><9 /th><th><2 Confidence><3 /th><th><6 Status><7 /th><kesalahan><0><1 /th><th><4 BucketId><5 /th></tr></thead><tbody><2 $deviceRows><3 /tbody></table></div>"             mengembalikan "$header$bucketSummary$deviceHeader$deviceTable"         }                  # Susun tabel sebaris dari file JSON yang sudah ada di disk, menautkan ke 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 ($dataDir Join-Path "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 ($dataDir "temp_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_temp_failures_$timestamp.csv"         $tblPerm = Build-InlineTable -JsonPath ($dataDir Join-Path "perm_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_perm_failures_$timestamp.csv"         $tblUpdated = Build-InlineTable -JsonPath ($dataDir Join-Path "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 ($dataDir Join-Path "under_observation.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_under_observation_$timestamp.csv"         $tblNeedsReboot = Build-InlineTable -JsonPath ($dataDir Join-Path "needs_reboot.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_needs_reboot_$timestamp.csv"         $tblSBOff = Build-InlineTable -JsonPath ($dataDir Join-Path "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"         # Tabel kustom untuk Pembaruan Tertunda — termasuk kolom UEFICA2023Status dan UEFICA2023Error         $tblUpdatePending = ""         $upJsonPath = Join-Path $dataDir "update_pending.json"         if (Test-Path $upJsonPath) {             coba {                 $upData = Get-Content $upJsonPath -Raw | ConvertFrom-Json                 $upCount = $upData.Count                 if ($upCount -gt 0) {                     $upHeader = "<div style='margin:5px 0; font-size:.85em; color:#666'>Total: $($upCount.ToString("N0")) perangkat | <a href='SecureBoot_update_pending_$timestamp.csv' style='color:#1a237e; font-weight:bold'>&#128196; Unduh CSV Lengkap untuk Excel><4 /a></div>"                     $upRows = ""                     $upSlice = $upData | Select-Object -$maxInlineRows pertama                     foreach ($d di $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>" } else { '-' }                         $policyVal = if ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } else { '-' }                         $wincsVal = if ($d.WinCSKeyApplied) { '<span class="badge-success">Ya><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 = [matematika]::Min($maxInlineRows, $upCount)                     $upDevHeader = "<h3 style='font-size:.95em; warna:#333; margin:10px 0 5px'>Detail Perangkat (memperlihatkan $upShowing pertama)</h3>"                     $upTable = "<div style='max-height:500px; overflow-y:tabel ><otomatis><ad><tr><th><9 HostName><0 /th><th><3 Produsen><4 /th><th><7 Model><8 /th><th><1 UEFICA2023Status><2 /th><th><5 UEFICA2023Error><6 /th><th><9 Kebijakan</th><>Kunci WinCS</th><th><<</th></tr></thead><tbody><5 $upRows><6 /tbody></table></div>"                     $tblUpdatePending = "$upHeader$upDevHeader$upTable"                 } lain {                     $tblUpdatePending = "<div style='padding:20px; warna:#888; gaya font:>Miring Tidak ada perangkat dalam kategori ini.</div>"                 }             } tangkap {                 $tblUpdatePending = "<div style='padding:20px; warna:#888; gaya font:>Miring Tidak ada perangkat dalam kategori ini.</div>"             }         } lain {             $tblUpdatePending = "<div style='padding:20px; warna:#888; gaya font:>Miring Tidak ada perangkat dalam kategori ini.</div>"         }                  # Penghitungan mundur kedaluwarsa # Cert         $certToday = Get-Date         $certKekExpiry = [datetime]"2026-06-24"         $certUefiExpiry = [datetime]"2026-06-27"         $certPcaExpiry = [datetime]"2026-10-19"         $daysToKek = [matematika]::Maks(0, ($certKekExpiry - $certToday). Hari)         $daysToUefi = [matematika]::Maks(0, ($certUefiExpiry - $certToday). Hari)         $daysToPca = [matematika]::Maks(0, ($certPcaExpiry - $certToday). Hari)         $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } else { '#28a745' }                  # Susun data bagan produsen sebaris (10 teratas menurut jumlah perangkat)         $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Turun | Select-Object -10 Pertama         $mfrChartTitle = if ($stMfrCounts.Count -le 10) { "By Manufacturer" } else { "Top 10 Manufacturers" }         $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_. Kunci)'" }) -join ","         $mfrUpdated = ($mfrSorted | ForEach-Object { $_. Value.Updated }) -join ","         $mfrUpdatePending = ($mfrSorted | ForEach-Object { $_. Value.UpdatePending }) -join ","         $mfrHighConf = ($mfrSorted | ForEach-Object { $_. Value.HighConf }) -join ","         $mfrUnderObs = ($mfrSorted | ForEach-Object { $_. Value.UnderObs }) -join ","         $mfrActionReq = ($mfrSorted | ForEach-Object { $_. Value.ActionReq }) -join ","         $mfrTempPaused = ($mfrSorted | ForEach-Object { $_. Value.TempPaused }) -join ","         $mfrNotSupported = ($mfrSorted | ForEach-Object { $_. Value.NotSupported }) -join ","         $mfrSBOff = ($mfrSorted | ForEach-Object { $_. Value.SBOff }) -join ","         $mfrWithErrors = ($mfrSorted | ForEach-Object { $_. Value.WithErrors }) -join ","                  # Susun tabel produsen         $mfrTableRows = ""         $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Turun | ForEach-Object {             $mfrTableRows += "<tr><td><7 $($_. Key)</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"         }                  $htmlContent = @" <!> html DOCTYPE <html lang="en"> ><3 kepala < <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> judul <><9 Dasbor Status Sertifikat Boot Aman><0 /judul><1 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script><5 ><7 gaya < *{box-sizing:border-box; margin:0; pengikisan:0} body{font-family:'Segoe UI',Tahoma,sans-serif; latar belakang:#f0f2f5; warna:#333} .header{background:linear-gradient(135deg,#1a237e,#0d47a1); warna:#fff; pengisian:20px 30px} .header h1{font-size:1.6em; margin-bottom:5px} .header .meta{font-size:.85em; keburaman:.9} .container{max-width:1400px; margin:0 otomatis; pengisian:20px} .cards{display:grid; grid-template-columns:repeat(auto-fill,minmax(170px,1fr)); jarak:12px; margin:20px 0} .card{background:#fff; radius batas:10px; pengisian:15px; box-shadow:0 2px 8px rgba(0,0,0,.08); border-left:4px solid #ccc;transisi:transformasi .2s} .card:hover{transform:translateY(-2px); box-shadow:0 4px 15px rgba(0,0,0,.12)} .card .value{font-size:1.8em; font-weight:700} .card .label{font-size:.8em; warna:#666; margin-top:4px} .card .pct{font-size:.75em; warna:#888} .section{background:#fff; radius batas:10px; pengisian:20px; margin:15px 0; box-shadow:0 2px 8px rgba(0,0,0,.08)} .section h2{font-size:1.2em; warna:#1a237e; margin-bottom:10px; kursor:penunjuk; pilih-pengguna:tidak ada} .section h2:hover{text-decoration:underline} .section-body{display:none} .section-body.open{display:block} .charts{display:grid; grid-template-columns:1fr 1fr; jarak:20px; margin:20px 0} .chart-box{background:#fff; radius batas:10px; pengisian:20px; box-shadow:0 2px 8px rgba(0,0,0,.08)} tabel{width:100%; batas-ciutkan:ciutkan; font-size:.85em} th{background:#e8eaf6; pengisian:8px 10px; text-align:left; posisi:tempel; atas:0; z-index:1} td{padding:6px 10px; border-bottom:1px solid #eee} tr:hover{background:#f5f5f5} .badge{display:inline-block; pengisian:2px 8px;radius batas:10px; font-size:.75em; font-weight:700} .badge-success{background:#d4edda; color:#155724} .badge-danger{background:#f8d7da; warna:#721c24} .badge-warning{background:#fff3cd; color:#856404} .badge-info{background:#d1ecf1; warna:#0c5460} .top-link{float:right; font-size:.8em; warna:#1a237e; text-decoration:none} .footer{text-align:center; pengisian:20px; warna:#999; font-size:.8em} a{color:#1a237e} </style><9 </head> > tubuh < <div class="header">     <h1>Dasbor Status Sertifikat Boot Aman</h1>     <div class="meta">Generated: $($stats. ReportGeneratedAt) | Total Perangkat: $($c.Total.ToString("N0")) | Bucket unik: $($stAllBuckets.Count)</div><3 </div><5 <div class="container">

Kartu KPI<!-- - dapat diklik, ditautkan ke bagian --> <div class="cards">     <a class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#dc3545; text-decoration:none; posisi:relative"><div style="position:absolute; atas:8px; kanan:8px; latar belakang:#dc3545; warna:#fff; pengisian:1px 6px; radius batas:8px; font-size:.65em; font-weight:700">PRIMARY</div><div class="value" style="color:#dc3545">$($stNotUptodate.ToString(""value N0"))</div><div class="label">NOT UPDATED><6 /div><div class="pct">$($stats. PercentNotUptodate)% - PERLU TINDAKAN><0 /div></a><3     <a class="card" href="#s-upd" onclick="openSection('d-upd')"style="border-left-color:#28a745; text-decoration:none; posisi:relative"><div style="position:absolute; atas:8px; kanan:8px; latar belakang:#28a745; warna:#fff; pengisian:1px 6px; radius batas:8px; font-size:.65em; font-weight:700">PRIMARY><8 /div><div class="value" style="color:#28a745">$($c.Updated.ToString(""color:#28a745">$($c.Updated.ToString(""N0"))</div><div class="label">Diperbarui><6 /div><div class="pct">$($stats. PersenCertUpdated)%</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; atas:8px; kanan:8px; latar belakang:#6c757d; warna:#fff; pengisian:1px 6px; radius batas:8px; font-size:.65em; font-weight:700">PRIMARY><8 /div><div class="value"><1 $($c.SBOff.ToString("N0"))><2 /div><div class="label"><5 SecureBoot OFF</div><div class="pct"><9 $(if($c.Total -gt 0){[math]::Round(($c.SBOff/$c.Total)*100,1)}else{0})% - Out of Scope><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){[matematika]:Round(($c.NeedsReboot/$c.Total)*100,1)}else{0})% - menunggu boot ulang><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 Tertunda</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.UpdatePending/$c.Total)*100,1)}else{0})% - Policy/WinCS diterapkan, menunggu pembaruan><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 /div><div class="pct ">$(if($c.Total -gt 0){[matematika]::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. PersenHighConfidence)% - Aman untuk peluncuran><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 Diperlukan><2 /div><div class="pct">$($stats. PersenActionRequired)% - harus menguji><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)% - Mirip dengan><2 /div></a><5 gagal     <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})% -><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. Dijeda</div><div class="pct">$(if($c.Total -gt 0){[matematika]: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">Masalah Umum><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)% - Kesalahan 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. Kegagalan</div><div class="pct">$(if($c.Total -gt 0){[matematika]::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">Tidak Didukung><6 /div><div class="pct">$(if($c.Total -gt 0){[matematika]::Round(($c.PermFailures/$c.Total)*100,1)}else{0})%</div></a><3 </div>

Kecepatan Penggunaan<!-- & Cert Kedaluwarsa --> <div id="s-velocity" style="display:grid; grid-template-columns:1fr 1fr; jarak:20px; margin:15px 0"> <div class="section" style="margin:0">     <h2>&#128197; Kecepatan Penyebaran</h2>     <div class="section-body open">         <div style="font-size:2.5em; font-weight:700; color:#28a745">$($c.Updated.ToString("N0"))</div>         <div style="color:#666">perangkat yang diperbarui dari $($c.Total.ToString("N0"))</div>         <div style="margin:10px 0; latar belakang:#e8eaf6; tinggi:20px; radius batas:10px; overflow:hidden"><div style="background:#28a745; tinggi:100%; width:$($stats. PersenCertUpdated)%; radius batas:10px"></div></div>         <div style="font-size:.8em; color:#888">$($stats. PersenCertUpdated)% lengkap</div>         <div style="margin-top:10px; pengisian:10px; latar belakang:#f8f9fa; radius batas:8px; font-size:.85em">             <><>kuat Sisa:</strong> $($stNotUptodate.ToString("N0")) perangkat memerlukan tindakan</div>             <div><strong>Blocking:</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated) perangkat (kesalahan + permanen + tugas nonaktif)</div>             <div><strong>Safe to deploy:</strong> $($stSafeList.ToString("N0")) perangkat (bucket yang sama berhasil)</div>             $velocityHtml         </div>     </div> </div> <div class="section" style="margin:0; border-left:4px solid #dc3545">     <h2 style="color:#dc3545">&#9888; Certificate Expiry Countdown</h2>     <div class="section-body open">         <div style="display:flex; jarak:15px; margin-top:10px">             <div style="text-align:center; pengisian:15px; radius batas:8px; lebar-min:120px; latar belakang:linear-gradient(135deg,#fff5f5,#ffe0e0); batas:#dc3545 solid 2px; flex:1">                 <div style="font-size:.65em; warna:#721c24; text-transform:uppercase; font-weight:bold">&#9888; FIRST TO EXPIRE</div>                 ><4 div style="font-size:.85em; font-weight:bold; warna:#dc3545; margin:3px 0"><5 KEK CA 2011</div>                 ><8 div id="daysKek" style="font-size:2.5em; font-weight:700; warna:#dc3545; tinggi-garis:1"><9 $daysToKek</div>                 ><2 div style="font-size:.8em; color:#721c24"><3 days (24 Jun 2026)><4 /div>             ><6 /div>             ><8 div style="text-align:center; pengisian:15px; radius batas:8px; lebar-min:120px; latar belakang:linear-gradient(135deg,#fffef5,#fff3cd); batas:#ffc107 solid 2px; flex:1"><9                 <div style="font-size:.65em; warna:#856404; text-transform:uppercase; font-weight:bold">UEFI CA 2011</div>                 <div id="daysUefi" style="font-size:2.2em; font-weight:700; warna:#856404; tinggi garis:1; margin:5px 0">$daysToUefi</div>                 <div style="font-size:.8em; color:#856404">days (27 Jun 2026)</div>             </div>             <div style="text-align:center; pengisian:15px; radius batas:8px; lebar-min:120px; latar belakang:linear-gradient(135deg,#f0f8ff,#d4edff); batas:#0078d4 padat 2px; flex:1">                 <div style="font-size:.65em; warna:#0078d4; text-transform:uppercase; font-weight:bold">Windows PCA</div>                 <div id="daysPca" style="font-size:2.2em; font-weight:700; warna:#0078d4; tinggi garis:1; margin:5px 0">$daysToPca><2 /div><3                 <div style="font-size:.8em; color:#0078d4">days (19 Okt 2026)</div><7             </div><9         </div><1         <div style="margin-top:15px; pengisian:10px; latar belakang:#f8d7da; radius batas:8px; font-size:.85em; batas kiri:4px solid #dc3545">             <strong>&#9888; CRITICAL:</strong> Semua perangkat harus diperbarui sebelum sertifikat kedaluwarsa. Perangkat yang tidak diperbarui oleh tenggat waktu tidak dapat menerapkan pembaruan Keamanan di masa mendatang untuk Boot Manager dan Boot Aman setelah kedaluwarsa.</div>     </div> </div> </div>

Bagan<!-- --> <div class="charts">     <div class="chart-box"><h3>Status Penyebaran</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) { "<!-- Historis Bagan Tren --> <div class='section'>     <h2 onclick='"toggle('d-trend')'">&#128200; Kemajuan Pembaruan Seiring Waktu <class='top-link' href='#'>&#8593; Top</a></h2>     <div id='d-trend' class='section-body open'>         <canvas id='trendChart' height='120'></canvas>         <div style='font-size:.75em; warna:#888; margin-top:5px'>Garis penuh = data aktual$(if ($historyData.Count -ge 2) { " | Garis putus-putus = diproyeksikan (penggambatan eksponensial: 2&#x2192;4&#x2192;8&#x2192;16... perangkat per gelombang)" } lainnya { " | Jalankan agregasi lagi besok untuk melihat garis tren dan proyeksi" })</div>     </div> </div>" })

unduhan CSV<!-- --> <div class="section">     <h2 onclick="toggle('dl-csv')">&#128229; Unduh Data Lengkap (CSV untuk Excel) <class="top-link" href="#">Top</a></h2><2     <div id="dl-csv" class="section-body open" style="display:flex; flex-wrap:wrap; gap:5px">         <href="SecureBoot_not_updated_$timestamp.csv" style="display:inline-block; latar belakang:#dc3545; warna:#fff; pengisian:6px 14px; radius batas:5px; text-decoration:none; font-size:.8em">Not Updated ($($stNotUptodate.ToString("N0")))</a><8         <a href="SecureBoot_errors_$timestamp.csv" style="display:inline-block; latar belakang:#dc3545; warna:#fff; pengisian:6px 14px; radius batas:5px; text-decoration:none; font-size:.8em">Errors ($($c.WithErrors.ToString("N0")))</a>         <href="SecureBoot_action_required_$timestamp.csv" style="display:inline-block; latar belakang:#fd7e14; warna:#fff; pengisian:6px 14px; radius batas:5px; text-decoration:none; font-size:.8em">Action Diperlukan ($($c.ActionReq.ToString("N0")))</a>         <a href="SecureBoot_known_issues_$timestamp.csv" style="display:inline-block; latar belakang:#dc3545; warna:#fff; pengisian:6px 14px; radius batas:5px; text-decoration:none; font-size:.8em">Masalah Umum ($($c.WithKnownIssues.ToString("N0")))</a>         <a href="SecureBoot_task_disabled_$timestamp.csv" style="display:inline-block; latar belakang:#dc3545; warna:#fff; pengisian:6px 14px; radius batas: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; latar belakang:#28a745; warna:#fff; pengisian:6px 14px; radius batas:5px; text-decoration:none; font-size:.8em">Updated ($($c.Updated.ToString("N0")))</a>         <href="SecureBoot_Summary_$timestamp.csv" style="display:inline-block; latar belakang:#6c757d; warna:#fff; pengisian:6px 14px; radius batas:5px; text-decoration:none; font-size:.8em">Ringkasan</a>         <div style="width:100%; font-size:.75em; warna:#888; margin-top:5px">file CSV terbuka di Excel. Tersedia ketika dihosting di server web.</div>     </div> </div>

Uraian Produsen<!-- --> <div class="section">     <h2 onclick="toggle('mfr')">Oleh Produsen <class="top-link" href="#">Top</a></h2><1     <div id="mfr" class="section-body open">     tabel <><ad><tr><th><1 Produsen><2 /th><th><5 Total><6 /th><th><9 Diperbarui><0 /th><th><3 Kepercayaan Diri Tinggi><4 /th><th><7 Tindakan diperlukan><8 /th></tr></thead><3     <><9><5 $mfrTableRows><6 /tbody></table     </div><1 </div>

<!-- Bagian Perangkat (200 inline + unduhan CSV pertama) --> <div class="section" id="s-err">     <h2 onclick="toggle('d-err')">&#128308; Perangkat dengan Kesalahan ($($c.WithErrors.ToString("N0"))) <class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-err" class="section-body">$tblErrors</div> </div> <div class="section" id="s-ki">     <h2 onclick="toggle('d-ki')" style="color:#dc3545">&#128308; Masalah yang Diketahui ($($c.WithKnownIssues.ToString("N0"))) <class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-ki" class="section-body">$tblKI</div> </div> <div class="section" id="s-kek">     <h2 onclick="toggle('d-kek')">&#128992; KEK yang Hilang - Kejadian 1803 ($($c.WithMissingKEK.ToString("N0"))) <class="top-link" href="#">&#8593; Top</a></h2>     >&#8593; 0 div id="d-kek" class="section-body">&#8593; 1 $tblKEK</div> >&#8593; 4 /div> >&#8593; 6 div class="section" id="s-ar">&#8593; 7     >&#8593; 8 h2 onclick="toggle('d-ar')" style="color:#fd7e14">&#128992; Tindakan Diperlukan ($($c.ActionReq.ToString("N0"))) <class="top-link" href="#">&#8593; Top><4 /a></h2><7     <div id="d-ar" class="section-body">$tblActionReq</div> </div> <div class="section" id="s-uo">     <h2 onclick="toggle('d-uo')" style="color:#17a2b8">&#128309; Di bawah Observasi ($($c.UnderObs.ToString("N0"))) <class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-uo" class="section-body">$tblUnderObs</div> </div> <div class="section" id="s-nu">     <h2 onclick="toggle('d-nu')" style="color:#dc3545">&#128308; Not Updated ($($stNotUptodate.ToString("N0"))) <a 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; Task Disabled ($($c.TaskDisabled.ToString("N0"))) >&#8593; 5 a 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; Kegagalan Sementara ($($c.TempFailures.ToString("N0"))) <class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-tf" class="section-body">$tblTemp</div> </div> <div class="section" id="s-pf">     <h2 onclick="toggle('d-pf')" style="color:#721c24">&#128308; Kegagalan Permanen / Tidak Didukung ($($c.PermFailures.ToString("N0"))) <class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-pf" class="section-body">$tblPerm</div> >/div < <div class="section" id="s-upd-pend">     <h2 onclick="toggle('d-upd-pend')" style="color:#6f42c1">&#9203; Update Pending ($($c.UpdatePending.ToString("N0"))) - Kebijakan/WinCS Diterapkan, Menunggu Pembaruan <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 atau 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; Rollout In Progress ($($c.RolloutInProgress.ToString("N0"))) <class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-rip" class="section-body">$tblRolloutIP</div> </div> <div class="section" id="s-sboff">     <h2 onclick="toggle('d-sboff')" style="color:#6c757d">&#9899; SecureBoot OFF ($($c.SBOff.ToString("N0"))) - Di Luar Lingkup <kelas="top-link" href="#">&#8593;<Atas /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; Perangkat yang Diperbarui ($($c.Updated.ToString("N0"))) <class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-upd" class="section-body">$tblUpdated</div> </div> <div class="section" id="s-nrb">     <h2 onclick="toggle('d-nrb')" style="color:#ffc107">&#128260; Diperbarui - Perlu Boot Ulang ($($c.NeedsReboot.ToString("N0"))) <class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-nrb" class="section-body">$tblNeedsReboot</div> </div>

<div class="footer">Secure Boot Certificate Rollout Dashboard | Menghasilkan $($stats. ReportGeneratedAt) | StreamingMode | Memori Puncak: ${stPeakMemMB} MB</div> </div><!-- /container -->

> skrip< fungsi toggle(id){var e=document.getElementById(id); e.classList.toggle('open')} fungsi openSection(id){var e=document.getElementById(id); if(e&&!e.classList.contains('open')){e.classList.add('open')}} Bagan baru(document.getElementById('deployChart'),{type:'doughnut',data:{labels:['Update','Update Pending','High Confidence','Under Observation','Action Diperlukan','Temp. Dijeda','Tidak Didukung','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','#6c757d','#721c24 #17a2b8','#6c757d',','#adb5bd','#dc3545']}]},options:{responsive:true,plugins:{legends:{position:'right',labels:{font:{size:11}}}}}}); Bagan baru(document.getElementById('mfrChart'),{type:'bar',data:{labels:[$mfrLabels],datasets:[{label:'Updated',data:[$mfrUpdated],backgroundColor:'#28a745'},{label:'Update Pending',data:[$mfrUpdatePending],backgroundColor:'#6f42c1'},{label:'Kepercayaan Tinggi',data:[$mfrHighConf],backgroundColor:'#20c997'},{label:'Under Observation',data:[$mfrUnderObs],backgroundColor:'#17a2b8'},{label:'Action Diperlukan',data:[$mfrActionReq],backgroundColor:'#fd7e14'},{ label:'Temp. Dijeda',data:[$mfrTempPaused],backgroundColor:'#6c757d'},{label:'Not Supported',data:[$mfrNotSupported],backgroundColor:'#721c24'},{label:'SecureBoot OFF',data:[$mfrSBOff],backgroundColor:'#adb5bd'},},{label:'With Errors',data:[$mfrWithErrors],backgroundColor:'#dc3545'}]},options:{responsive:true,scales:{x:{stacked:true},y:{stacked:true}},plugins:{legend:{position:'top'}}}}); Bagan Tren Riwayat 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); } set data var = [     {label:'Updated',data:paddedUpdated,borderColor:'#28a745',backgroundColor:'rgba(40,167,69,0.1)',fill:true,tension:0.3,borderWidth:2},     {label:'Not Updated',data:paddedNotUpdated,borderColor:'#dc3545',backgroundColor:'rgba(220,53,69,0.1)',fill:true,tension:0.3,borderWidth:2},     {label:'Total',data:paddedTotal,borderColor:'#6c757d',borderDash:[5,5],fill:false,tension:0,pointRadius:0,borderWidth:1} ]; 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'}); } Bagan baru(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'}}}}); } Hitungan mundur dinamis (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))})(); > /skrip < </body> </html> "@         [System.IO.File]::WriteAllText($htmlPath, $htmlContent, [System.Text.UTF8Encoding]::new($false))         # Selalu simpan salinan "Terbaru" yang stabil sehingga admin tidak perlu melacak stempel waktu         $latestPath = Join-Path $OutputPath "SecureBoot_Dashboard_Latest.html"         Copy-Item $htmlPath $latestPath -Force         $stTotal = $streamSw.Elapsed.TotalSeconds         # Simpan manifes file untuk mode tambahan (deteksi tanpa perubahan cepat pada jalankan berikutnya)         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 "Menyimpan manifes file untuk mode penambahan..." -ForegroundColor Gray             foreach ($jf di $jsonFiles) {                 $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{                     LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o")                     Ukuran = $jf. Panjang                 }             }             Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath             Write-Host " Manifes tersimpan untuk file $($stNewManifest.Count) " -ForegroundColor DarkGray         }         # PEMBERSIHAN PENYIMPANAN         # Folder orkestrator yang dapat digunakan kembali (Aggregation_Current): hanya jalankan terbaru (1)         # Admin jalankan manual / folder lainnya: terus jalankan 7 kali terakhir         # CSV Ringkasan TIDAK PERNAH dihapus — CSV kecil (~1 KB) dan merupakan sumber cadangan untuk riwayat tren         $outputLeaf = Split-Path $OutputPath -Leaf         $retentionCount = if ($outputLeaf -eq 'Aggregation_Current') { 1 } else { 7 }         # Prefiks file aman untuk dibersihkan (snapshot ephemeral per-run)         $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_'         )         # Temukan semua stempel waktu unik dari file yang dapat dibersihkan saja         $cleanableFiles = Get-ChildItem $OutputPath -File -EA SilentlyContinue |             Where-Object { $f = $_. Nama; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Hitung -gt 0 }         $allTimestamps = @($cleanableFiles | ForEach-Object {             jika ($_. Nama -match '(\d{8}-\d{6})') { $Matches[1] }         } | Sort-Object -Unique -Descending)         if ($allTimestamps.Count -gt $retentionCount) {             $oldTimestamps = $allTimestamps | Select-Object -Lewati $retentionCount             $removedFiles = 0; $freedBytes = 0             foreach ($oldTs di $oldTimestamps) {                 foreach ($prefix di $cleanupPrefixes) {                     $oldFiles = Get-ChildItem $OutputPath -File -Filter "${prefix}${oldTs}*" -EA SilentlyContinue                     foreach ($f di $oldFiles) {                         $freedBytes += $f.Length                         Remove-Item $f.FullName -Force -EA SilentlyContinue                         $removedFiles++                     }                 }             }             $freedMB = [matematika]::Round($freedBytes / 1MB, 1)             Write-Host "Pembersihan penyimpanan: menghapus file $removedFiles dari $($oldTimestamps.Count) yang berjalan lama, membebaskan ${freedMB} MB (menyimpan $retentionCount terakhir + semua Ringkasan/NotUptodate CSV)" -ForegroundColor DarkGray         }         Write-Host "'n$("=" * 60)" -ForegroundColor Sian         Write-Host "STREAMING AGREGATION COMPLETE" -ForegroundColor Green         Write-Host ("=" * 60) -ForegroundColor Sian         Write-Host " Total Perangkat: $($c.Total.ToString("N0"))" -ForegroundColor White         Write-Host " TIDAK DIPERBARUI: $($stNotUptodate.ToString("N0")) ($($stats. PercentNotUptodate)%)" -ForegroundColor $(if ($stNotUptodate -gt 0) { "Yellow" } else { "Green" })         Write-Host " Diperbarui: $($c.Updated.ToString("N0")) ($($stats. PersenCertUpdated)%)" -ForegroundColor Green         Write-Host " Dengan Kesalahan: $($c.WithErrors.ToString("N0"))" -ForegroundColor $(if ($c.WithErrors -gt 0) { "Red" } else { "Green" })         Write-Host " Memori Puncak: ${stPeakMemMB} MB" -ForegroundColor Cyan         Write-Host " Waktu: $([matematika]:Round($stTotal/60,1)) mnt" -ForegroundColor White         Write-Host " Dasbor: $htmlPath" -ForegroundColor White         return [PSCustomObject]$stats     }     mode streaming #endregion } lain {     Write-Error "Jalur input tidak ditemukan: $InputPath"     keluar 1 }                                                      

Perlu bantuan lainnya?

Ingin opsi lainnya?

Jelajahi manfaat langganan, telusuri kursus pelatihan, pelajari cara mengamankan perangkat Anda, dan banyak lagi.