Copiați și lipiți acest script eșantion și modificați după cum este necesar pentru mediul dvs.:

<# . SINOPSIS     Rezuma datele JSON ale stării de bootare securizată de pe mai multe dispozitive în rapoarte rezumative.

. . DESCRIEREA /     Citește fișierele JSON de stare secure boot colectate și generează:     - Tablou de bord HTML cu diagrame și filtrare     - Rezumat după Nivelul de încredere     - Analiza unică a bucketului dispozitivului pentru strategia      de testare     Acceptă:     - Fișiere pe fiecare computer: HOSTNAME_latest.json (recomandat)     - Fișier      JSON unic     Se anulează automat asocierea de către HostName, păstrând cea mai recentă CollectionTime.     În mod implicit, include doar dispozitivele cu încredere "Action Req" sau "High"     pentru a vă concentra pe recipientele acționabile. Utilizați -IncludeAllConfidenceLevels pentru înlocuire.

. . CALE INTRARE PARAMETRU     Cale către fișiere JSON:     - Folder: Citește toate fișierele *_latest.json (sau *.json dacă nu există fișiere _latest)     - Fișier: citește un singur fișier JSON

. . Parameter OutputPath     Calea pentru rapoartele generate (implicit: .\SecureBootReports)

. . EXEMPLU     # Agregați din folderul fișierelor per computer (recomandat)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$"     # Citește: \\contoso\SecureBootLogs$\*_latest.json

. . EXEMPLU     # Locație de ieșire particularizată     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -OutputPath "C:\Reports\SecureBoot"

. . EXEMPLU     # Includeți doar acțiunile de încredere maximă (comportament implicit)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$"     # Excluderi: observare, în pauză, neacceptat

. . EXEMPLU     # Includeți toate nivelurile de încredere (înlocuiți filtrul)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeAllConfidenceLevels

. . EXEMPLU     # Filtru particularizat de nivel de încredere     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeConfidenceLevels @("Action Req", "High", "Observation")

. . EXEMPLU     # ENTERPRISE SCALE: Modul incremental - doar fișiere modificate de proces (rulări ulterioare rapide)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode     # Prima rulare: Încărcare completă ~ 2 ore pentru dispozitive 500K     # Următoarele rulează: Secunde dacă nu există modificări, minute pentru delta

. . EXEMPLU     # Skip HTML if nothing changed (fastest for monitoring)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode -SkipReportIfUnchanged     # Dacă nu s-au modificat fișiere de la ultima rulare: ~5 secunde

. . EXEMPLU     # Modul doar rezumat - săriți peste tabelele de dispozitive mari (1-2 minute versus 20+ minute)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -SummaryOnly     # Generează CSV-uri, dar ignoră tabloul de bord HTML cu tabele de dispozitive complete

. . NOTE     Asociați-vă cu Detect-SecureBootCertUpdateStatus.ps1 pentru implementarea la nivel de întreprindere.Consultați GPO-DEPLOYMENT-GUIDE.md pentru ghidul complet de implementare.     Comportamentul implicit exclude dispozitivele de observație, în pauză și neacceptate     pentru a concentra raportarea doar asupra bucketurilor de dispozitiv acționabile.#>

param(     [Parametru(Obligatoriu = $true)]     [șir]$InputPath,          [Parametru(Obligatoriu = $false)]     [string]$OutputPath = ".\SecureBootReports",          [Parametru(Obligatoriu = $false)]     [string]$ScanHistoryPath = ".\SecureBootReports\ScanHistory.json",          [Parametru(Obligatoriu = $false)]     [string]$RolloutStatePath, # Path to RolloutState.json to identify InProgress devices          [Parametru(Obligatoriu = $false)]     [string]$RolloutSummaryPath, # Path to SecureBootRolloutSummary.json from Orchestrator (conține date de proiecție)          [Parametru(Obligatoriu = $false)]     [string[]]$IncludeConfidenceLevels = @("Acțiune necesară", "Nivel înalt de încredere"), # Includeți numai aceste niveluri de încredere (implicit: numai bucketuri care pot fi acționate)          [Parametru(Obligatoriu = $false)]     [switch]$IncludeAllConfidenceLevels, # Înlocuire filtru pentru a include toate nivelurile de      încredere     [Parametru(Obligatoriu = $false)]     [comutator]$SkipHistoryTracking,          [Parametru(Obligatoriu = $false)]     [switch]$IncrementalMode, # Enable delta processing - only load changed files since last run          [Parametru(Obligatoriu = $false)]     [string]$CachePath, # Path to cache directory (implicit: OutputPath\.cache)          [Parametru(Obligatoriu = $false)]     [int]$ParallelThreads = 8, # Numărul de fire paralele pentru încărcarea fișierelor (PS7+)          [Parametru(Obligatoriu = $false)]     [switch]$ForceFullRefresh, # Impuneți reîncărcarea completă chiar și în modul      incremental     [Parametru(Obligatoriu = $false)]     [switch]$SkipReportIfUnchanged, # Ignorare generație HTML/CSV dacă nu s-au modificat fișiere (doar statistici de ieșire)          [Parametru(Obligatoriu = $false)]     [switch]$SummaryOnly, # Generați doar statistici rezumat (nu există tabele mari de dispozitive) - mult mai rapid          [Parametru(Obligatoriu = $false)]     [switch]$StreamingMode # Modul eficient din punct de vedere al memoriei: segmente de proces, scriere CSV-uri incremental, păstrarea doar a rezumatelor în memorie )

# Elevare automată la PowerShell 7 dacă este disponibilă (de 6 ori mai rapidă pentru seturile de date mari) dacă ($PSVersionTable.PSVersion.Major -lt 7) {     $pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object - Extindere SursăProprietate     dacă ($pwshPath) {         Write-Host a detectat "PowerShell $($PSVersionTable.PSVersion) - re-lansarea cu PowerShell 7 pentru procesare mai rapidă..." -Culoare prim plan galbenă         # Rebuild argument list from bound parameters         $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $MyInvocation.MyCommand.Path)         pentru a căuta ($key în $PSBoundParameters.Keys) {             $val = $PSBoundParameters[$key]             dacă ($val -este [comutator]) {                 dacă ($val. IsPresent) { $relaunchArgs += "-$key" }             } elseif ($val -is [matrice]) {                 $relaunchArgs += "-$key"                 $relaunchArgs += ($val -join ',')             } altfel, {                 $relaunchArgs += "-$key"                 $relaunchArgs += "$val"             }         }         & $pwshPath @relaunchArgs         ieșire din $LASTEXITCODE     } }

$ErrorActionPreference = "Continuare" $timestamp = Get-Date - Format "yyyyMMdd-HHmmss" $scanTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Eșantioane de implementare și monitorizare"

# Notă: Acest script nu are dependențe de alte scripturi. # Pentru set de instrumente complet, descărcați de la: $DownloadUrl -> $DownloadSubPage

instalare #region Write-Host "=" * 60 -Prim planColor Cyan Write-Host "Agregare date de bootare securizată" -Prim-planColor Cyan Write-Host "=" * 60 -Prim planColor Cyan

# Creare director de rezultate dacă (-nu (Test-Cale $OutputPath)) {     New-Item -ItemType Directory -Cale $OutputPath -Force | Nul în exterior }

# Încărcare date - acceptă formate CSV (moștenite) și JSON (native) Write-Host "'nÎncărcarea datelor din: $InputPath" -Culoare prim plan galben

# Funcția Helper pentru normalizarea obiectului dispozitivului (handle field name differences) funcție Normalize-DeviceRecord {     param($device)          # Handle Hostname vs HostName (JSON utilizează Hostname, CSV uses HostName)     dacă ($device. PSObject.Properties['Hostname'] -and -not $device. PSObject.Properties['HostName']) {         $device | Add-Member -NotePropertyName 'HostName' -NotePropertyValue $device. Hostname -Force     }          # Handle Confidence vs ConfidenceLevel (JSON utilizează Confidence, CSV uses ConfidenceLevel)     # ConfidenceLevel este numele oficial al câmpului - hartă Încredere în el     dacă ($device. PSObject.Properties['Confidence'] -and -not $device. PSObject.Properties['ConfidenceLevel']) {         $device | Add-Member -NotePropertyName "ConfidenceLevel" -NotePropertyValue $device. Încredere - Forțare     }          # Urmăriți starea actualizării prin Event1808Count SAU UEFICA2023Status="Updated"     # Acest lucru permite urmărirea numărului de dispozitive din fiecare bucket de încredere care au fost actualizate     $event 1808 = 0     dacă ($device. PSObject.Properties['Event1808Count']) {         $event 1808 = [int]$device. Eveniment1808Count     }     $uefiCaUpdated = $false     dacă ($device. PSObject.Properties['UEFICA2023Status'] -and $device. UEFICA2023Status -eq "Actualizat") {         $uefiCaUpdated = $true     }          dacă ($event 1808 -gt 0 -sau $uefiCaUpdated) {         # Marcați ca actualizat pentru logica tablou de bord/implementare - dar NU înlocuiți Nivelul de încredere         $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $true -Force     } altfel, {         $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $false -Force                  # ConfidenceLevel clasificare:         # - "High Confidence", "Under observation...", "Temporar paused...", "Not Supported..." = use as-is         # - Orice altceva (nul, gol, "UpdateType:...", "Necunoscut", "N/A") = se încadrează în Acțiune necesară în contoare         # Nu este necesară normalizarea - ramura contorului de redare în flux altceva se ocupă de ea     }          # Handle OEMManufacturerName vs WMI_Manufacturer (JSON utilizează OEM*, moștenit utilizează WMI_*)     dacă ($device. PSObject.Properties['OEMManufacturerName'] -and -not $device. PSObject.Properties['WMI_Manufacturer']) {         $device | Add-Member -NotePropertyName "WMI_Manufacturer" -NotePropertyValue $device. OEMManufacturerName -Force     }          # Handle OEMModelNumber vs WMI_Model     dacă ($device. PSObject.Properties['OEMModelNumber'] -and -not $device. PSObject.Properties['WMI_Model']) {         $device | Add-Member -NotePropertyName "WMI_Model" -NotePropertyValue $device. OEMModelNumber -Force     }          # Handle FirmwareVersion vs BIOSDescription     dacă ($device. PSObject.Properties['FirmwareVersion'] -and -not $device. PSObject.Properties['BIOSDescription']) {         $device | Add-Member -NotePropertyName 'BIOSDescription' -NotePropertyValue $device. FirmwareVersion -Force     }          $device de returnare }

#region procesare incrementală / Gestionare cache # Configurare căi cache dacă (-nu $CachePath) {     $CachePath = Join-Path $OutputPath ".cache" } $manifestPath = Join-Path $CachePath "FileManifest.json" $deviceCachePath = Join-Path $CachePath "DeviceCache.json"

# Funcții de gestionare a memoriei cache funcție Get-FileManifest {     param([șir]$Path)     if (Test-Cale $Path) {         încercați {             $json = Get-Content $Path -Brut | ConvertFrom-Json             # Convert PSObject to hashtable (compatibil PS5.1 - PS7 has -AsHashtable)             $ht = @{}             $json. PSObject.Properties | ForEach-Object { $ht[$_. Nume] = $_. Valoare }             $ht de returnare         } captură {             returnează @{}         }     }     returnează @{} }

funcție Save-FileManifest {     param([hashtable]$Manifest, [șir]$Path)     $dir = Split-Path $Path -Părinte     if (-not (Test-Path $dir)) {         New-Item -ItemType Directory -Cale $dir -Force | Nul în exterior     }     $Manifest | ConvertTo-Json -Adâncime 3 -Comprimare | Set-Content $Path - Forțare }

funcție Get-DeviceCache {     param([șir]$Path)     if (Test-Cale $Path) {         încercați {             $cacheData = Get-Content $Path -Brut | ConvertFrom-Json             Write-Host " Memorie cache de dispozitiv încărcată: dispozitive $($cacheData.Count) " -Prim-planColor DarkGray             $cacheData de returnare         } captură {             Write-Host " Cache deteriorat, se va reconstrui" -Prim-planColor galben             returnare @()         }     }     returnare @() }

funcție Save-DeviceCache {     param($Devices, [șir]$Path)     $dir = Split-Path $Path -Părinte     if (-not (Test-Path $dir)) {         New-Item -ItemType Directory -Cale $dir -Force | Nul în exterior     }     # Conversie în matrice și salvare     $deviceArray = @($Devices)     $deviceArray | ConvertTo-Json -Adâncime 10 -Comprimare | Set-Content $Path - Forțare     Write-Host " Memorie cache de dispozitiv salvată: dispozitive $($deviceArray.Count) " -Prim-planColor DarkGray }

funcție Get-ChangedFiles {     param(         [System.IO.FileInfo[]]$AllFiles,         [hashtable]$Manifest     )          $changed = [System.Collections.ArrayList]::new()     $unchanged = [System.Collections.ArrayList]::new()     $newManifest = @{}          # Generați căutarea fără sensibilitate la litere mari și mici din manifest (normalizare la litere mici)     $manifestLookup = @{}     pentru a căuta ($mk în $Manifest.Keys) {         $manifestLookup[$mk. ToLowerInvariant()] = $Manifest[$mk]     }          pentru fiecare ($file din $AllFiles) {         $key = $file. FullName.ToLowerInvariant() # Normalizați calea la litere mici         $lwt = $file. LastWriteTimeUtc.ToString("o")         $newManifest[$key] = @{             LastWriteTimeUtc = $lwt             Dimensiune = $file. Lungime         }                  if ($manifestLookup.ContainsKey($key)) {             $cached = $manifestLookup[$key]             dacă ($cached. LastWriteTimeUtc -eq $lwt -și $cached. Dimensiune -eq $file. Lungime) {                 [nul]$unchanged. Adăugare($file)                 Continua             }         }         [nul]$changed. Adăugare($file)     }          returnează @{         Modificat = $changed         Neschimbat = $unchanged         NewManifest = $newManifest     } }

# Ultra-rapid de încărcare a fișierelor paralele utilizând procesarea pe loturi funcție Load-FilesParallel {     param(         [System.IO.FileInfo[]]$Files,         [int]$Threads = 8     )

$totalFiles = Files de lei. Conta     # Utilizați seturi de ~1000 de fișiere fiecare pentru un control mai bun al memoriei     $batchSize = [matematică]::Min(1000, [matematică]:Ceiling($totalFiles / [matematică]:Max(1, $Threads)))     $batches = [System.Collections.Generic.List[obiect]]:new()     

pentru ($i = 0; $i -lt $totalFiles; $i += $batchSize) {         $end = [matematică]::Min($i + $batchSize, $totalFiles)         $batch = Files lei[$i.) ($end-1)]         $batches. Adăugare($batch)     }     Write-Host " ($($batches. Count) loturi de ~$batchSize fiecare)" -NoNewline -ForegroundColor DarkGray     $flatResults = [System.Collections.Generic.List[obiect]]:new()     # Verificați dacă PowerShell 7+ parallel este disponibil     $canParallel = $PSVersionTable.PSVersion.Major -ge 7     dacă ($canParallel -și $Threads -gt 1) {         # PS7+: Procesarea loturilor în paralel         $results = $batches | ForEach-Object -ThrottleLimit $Threads -Parallel {             $batchFiles = $_             $batchResults = [System.Collections.Generic.List[obiect]]:new()             pentru a căuta ($file în $batchFiles) {                 încercați {                     $content = [System.IO.File]::ReadAllText($file. NumeComplet) | ConvertFrom-Json                     $batchResults.Add($content)                 } captură { }             }             $batchResults.ToArray()         }         pentru a căuta ($batch în $results) {             if ($batch) { foreach ($item in $batch) { $flatResults.Add($item) } }         }     } altfel, {         # PS5.1 rezervă: Sequential de prelucrare (încă mai rapid pentru <fișiere 10K)         foreach ($file in $Files) {             încercați {                 $content = [System.IO.File]::ReadAllText($file. NumeComplet) | ConvertFrom-Json                 $flatResults.Add($content)             } captură { }         }     }     returnează $flatResults.ToArray() } #endregion                         

$allDevices = @() if (Test-Path $InputPath -PathType Leaf) {     # Single JSON file     dacă ($InputPath -like "*.json") {         $jsonContent = Get-Content -Cale $InputPath -Brut | ConvertFrom-Json         $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ }         Write-Host "S-au încărcat înregistrări $($allDevices.Count) din fișier"     } altfel, {         Write-Error "Se acceptă numai formatul JSON. Fișierul trebuie să aibă extensia .json."         ieșire 1     } } elseif (Cale-test $InputPath -Container PathType) {     # Folder - numai JSON     $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue |                    Where-Object { $_. Name -notmatch "ScanHistory|RolloutState|RolloutPlan" })          # Preferați *_latest.json fișiere dacă există (modul per computer)     $latestJson = $jsonFiles | Where-Object { $_. Nume - cum ar fi "*_latest.json" }     dacă ($latestJson.Count -gt 0) { $jsonFiles = $latestJson }          $totalFiles = $jsonFiles.Count          dacă ($totalFiles -eq 0) {         Write-Error "Nu s-au găsit fișiere JSON în: $InputPath"         ieșire 1     }          Write-Host "S-au găsit fișiere JSON $totalFiles" -Prim planColor Gri          # Funcția Helper pentru a corespunde nivelurilor de încredere (gestionează atât forme scurte, cât și complete)     # Definit timpuriu, astfel încât atât StreamingMode, cât și căile normale să îl poată utiliza     funcție Test-ConfidenceLevel {         param([șir]$Value, [șir]$Match)         if ([string]::IsNullOrEmpty($Value)) { returnează $false }         comutator ($Match) {             "HighConfidence" { returnează $Value -eq "High Confidence" }             "UnderObservation" { return $Value -like "Under Observation*" }             "ActionRequired" { return ($Value -like "*Acțiune necesară*" -sau $Value -eq "Acțiune necesară") }             "TemporarPaused" { returnează $Value -cum ar fi "Temporar în pauză*" }             "Neacceptat" { return ($Value -like "Neacceptat*" -sau $Value -eq "Neacceptat") }             { return $false } implicit         }     }          #region MODUL DE REDARE ÎN FLUX - procesare eficientă a memoriei pentru seturi mari de date     # Utilizați întotdeauna StreamingMode pentru procesarea eficientă a memoriei și tabloul de bord în stil nou     dacă (-nu $StreamingMode) {         Write-Host "Activare automată StreamingMode (tablou de bord în stil nou)" -Prim planColor galben         $StreamingMode = $true         dacă (-nu $IncrementalMode) { $IncrementalMode = $true }     }          # Atunci când este activat StreamingMode, procesați fișierele în bucăți menținând doar contoare în memorie.# Datele la nivel de dispozitiv sunt scrise în fișiere JSON per bloc pentru încărcarea la cerere în tabloul de bord.# Utilizare memorie: ~1,5 GB indiferent de dimensiunea setului de date (versus 10-20 GB fără redare în flux).dacă ($StreamingMode) {         Write-Host "STREAMING MODE enabled - memory-efficient processing" -ForegroundColor Green         $streamSw = [System.Diagnostics.Stopwatch]::StartNew()         # INCREMENTAL CHECK: Dacă nu s-au modificat fișiere de la ultima rulare, omiteți procesarea în întregime         dacă ($IncrementalMode -și -nu $ForceFullRefresh) {             $stManifestDir = Join-Path $OutputPath ".cache"             $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json"             if (Test-Cale $stManifestPath) {                 Write-Host "Se verifică modificările de la ultima rulare în flux..." -Prim-planColor Cyan                 $stOldManifest = Get-FileManifest - $stManifestPath cale                 dacă ($stOldManifest.Count -gt 0) {                     $stChanged = $false                     # Verificare rapidă: același număr de fișiere?                     dacă ($stOldManifest.Count -eq $totalFiles) {                         # Verificați cele mai noi 100 de fișiere (sortate după LastWriteTime descendent)                         # Dacă un fișier s-a modificat, acesta va avea cea mai recentă marcă de timp și va apărea primul                         $sampleSize = [matematică]::Min(100, $totalFiles)                         $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc -Descendent | Select-Object - primul $sampleSize                         foreach ($sf în $sampleFiles) {                             $sfKey = $sf. NumeComplet.ToLowerInvariant()                             if (-not $stOldManifest.ContainsKey($sfKey)) {                                 $stChanged = $true                                 Pauză                             }                             # Compare timestamps - memorate în cache pot fi DateTime sau șir după JSON roundtrip                             $cachedLWT = $stOldManifest[$sfKey]. LastWriteTimeUtc                             $fileDT = $sf. LastWriteTimeUtc                             încercați {                                 # Dacă memorate în cache este deja DateTime (ConvertFrom-Json auto-converts), utilizați direct                                 dacă ($cachedLWT -este [DateTime]) {                                     $cachedDT = $cachedLWT.ToUniversalTime()                                 } altfel, {                                     $cachedDT = [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime                                 }                                 if ([matematică]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) {                                     $stChanged = $true                                     Pauză                                 }                             } captură {                                 $stChanged = $true                                 Pauză                             }                         }                     } altfel, {                         $stChanged = $true                     }                     dacă (-nu $stChanged) {                         # Verificați dacă există fișiere de ieșire                         $stSummaryExists = Get-ChildItem ($OutputPath "SecureBoot_Summary_*.csv") -EA SilentlyContinue | Select-Object -Primul 1                         $stDashExists = Get-ChildItem ($OutputPath "SecureBoot_Dashboard_*.html") -EA SilentlyContinue | Select-Object -Primul 1                         dacă ($stSummaryExists și $stDashExists) {                             Write-Host " Nu s-au detectat modificări ($totalFiles fișiere neschimbate) - se ignoră procesarea" -Prim-planColor verde                             Write-Host " Ultimul tablou de bord: $($stDashExists.FullName)" -Prim-planColor Alb                             $cachedStats = Get-Content $stSummaryExists.NumeComplet | ConvertFrom-Csv                             Write-Host " Dispozitive: $($cachedStats.TotalDevices) | Actualizat: $($cachedStats.Updated) | Erori: $($cachedStats.WithErrors)" -Prim-planColor gri                             Write-Host " Terminat în $([matematică]::Round($streamSw.Elapsed.TotalSeconds, 1))s (nu este necesară nicio procesare)" -Prim-planColor Verde                             $cachedStats de returnare                         }                     } altfel, {                         # DELTA PATCH: Aflați exact ce fișiere s-au modificat                         Write-Host " Modificări detectate - se identifică fișierele modificate..." -Prim-planColor galben                         $changedFiles = [System.Collections.ArrayList]::new()                         $newFiles = [System.Collections.ArrayList]::new()                         pentru a căuta ($jf în $jsonFiles) {                             $jfKey = $jf. NumeComplet.ToLowerInvariant()                             if (-not $stOldManifest.ContainsKey($jfKey)) {                                 [nul]$newFiles.Add($jf)                             } altfel, {                                 $cachedLWT = $stOldManifest[$jfKey]. LastWriteTimeUtc                                 $fileDT = $jf. LastWriteTimeUtc                                 încercați {                                     $cachedDT = dacă ($cachedLWT -este [DateTime]) { $cachedLWT.ToUniversalTime() } altfel{ [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime }                                     if ([matematică]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { [nud]$changedFiles.Add($jf) }                                 } captură { [void]$changedFiles.Add($jf) }                             }                         }                         $totalChanged = $changedFiles.Count + $newFiles.Count                         $changePct = [matematică]:Round(($totalChanged / $totalFiles) * 100, 1)                         Write-Host " Modificat: $($changedFiles.Count) | Nou: $($newFiles.Count) | Total: $totalChanged ($changePct%)" -Culoare prim plan galben                         dacă ($totalChanged -gt 0 și $changePct -lt 10) {                             # DELTA PATCH MODE: <10% modificat, patch-uri de date existente                             Write-Host " Modul de corecție Delta ($changePct% < 10%) - se corectează fișierele $totalChanged..." -Prim planColor Verde                             $dataDir = Join-Path $OutputPath "date"                             # Încărcați înregistrările modificate/noi de dispozitiv                             $deltaDevices = @{}                             $allDeltaFiles = @($changedFiles) + @($newFiles)                             pentru a căuta ($df în $allDeltaFiles) {                                 încercați {                                     $devData = Get-Content $df. FullName -Raw | ConvertFrom-Json                                     $dev = Normalize-DeviceRecord $devData                                     dacă ($dev. HostName) { $deltaDevices[$dev. HostName] = $dev }                                 } captură { }                             }                             Write-Host " S-au încărcat înregistrările de dispozitiv $($deltaDevices.Count) modificate" -Prim-planColor gri                             # Pentru fiecare categorie JSON: eliminați intrările vechi pentru numele de gazdă modificate, adăugați intrări noi                             $categoryFiles = @("erori", "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 în $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) }                             pentru fiecare ($cat din $categoryFiles) {                                 $catPath = Join-Path $dataDir "$cat.json"                                 if (Test-Path $catPath) {                                     încercați {                                         $catData = Get-Content $catPath -Brut | ConvertFrom-Json                                         # Eliminați intrările vechi pentru numele de gazdă modificate                                         $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. Nume Gazdă) })                                         # Re clasificați fiecare dispozitiv modificat în categorii                                         # (vor fi adăugate mai jos după clasificare)                                         $catData | ConvertTo-Json -Adâncime 5 | Set-Content $catPath -codificare UTF8                                     } captură { }                                 }                             }                             # Clasificați fiecare dispozitiv modificat și adăugați la fișierele categoriei potrivite                             foreach ($dev în $deltaDevices.Values) {                                 $slim = [ordonat]@{                                     HostName = $dev. Hostname                                     WMI_Manufacturer = dacă ($dev. PSObject.Properties['WMI_Manufacturer']) { $dev. WMI_Manufacturer } altceva { "" }                                     WMI_Model = dacă ($dev. PSObject.Properties['WMI_Model']) { $dev. WMI_Model } altceva { "" }                                     BucketId = if ($dev. PSObject.Properties['BucketId']) { $dev. BucketId } altfel { "" }                                     ConfidenceLevel = dacă ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" }                                     IsUpdated = $dev. IsUpdated                                     UEFICA2023Eroare = dacă ($dev. PSObject.Properties['UEFICA2023Error']) { $dev. UEFICA2023Error } altceva { $null }                                     SecureBootTaskStatus = if ($dev. PSObject.Properties['SecureBootTaskStatus']) { $dev. SecureBootTaskStatus } altceva { "" }                                     KnownIssueId = if ($dev. PSObject.Properties['KnownIssueId']) { $dev. KnownIssueId } else { $null }                                     SkipReasonKnownIssue = dacă ($dev. PSObject.Properties['SkipReasonKnownIssue']) { $dev. SkipReasonKnownIssue } else { $null }                                 }                                 $isUpd = $dev. IsUpdated -eq $true                                 $conf = dacă ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" }                                 $hasErr = (-nu [șir]::IsNullOrEmpty($dev. UEFICA2023Error) -și $dev. UEFICA2023Error -ne "0" -și $dev. UEFICA2023Error -ne "")                                 $tskDis = ($dev. SecureBootTaskEnabled -eq $false -sau $dev. SecureBootTaskStatus -eq "Dezactivat" -sau $dev. SecureBootTaskStatus -eq 'NotFound')                                 $tskNF = ($dev. SecureBootTaskStatus -eq 'NotFound')                                 $sbOn = ($dev. SecureBootEnabled -ne $false -și "$($dev. SecureBootEnabled)" -ne "False")                                 $e 1801 = dacă ($dev. PSObject.Properties['Event1801Count']) { [int]$dev. Eveniment1801Count } altceva { 0 }                                 $e 1808 = dacă ($dev. PSObject.Properties['Event1808Count']) { [int]$dev. Eveniment1808Count } altceva { 0 }                                 $e 1803 = dacă ($dev. PSObject.Properties['Event1803Count']) { [int]$dev. Eveniment1803Count } altfel { 0 }                                 $mKEK = ($e 1803 -gt 0 -sau $dev. MissingKEK -eq $true)                                 $hKI = ((nu [șir]::IsNullOrEmpty($dev. SkipReasonKnownIssue)) -sau (-nu [șir]::IsNullOrEmpty($dev. KnownIssueId)))                                 $rStat = dacă ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } else { "" }                                 # Adăugare la fișierele categorie care se potrivesc                                 $targets = @()                                 dacă ($isUpd) { $targets += "updated_devices" }                                 dacă ($hasErr) { $targets += "erori" }                                 dacă ($hKI) { $targets += "known_issues" }                                 dacă ($mKEK) { $targets += "missing_kek" }                                 dacă (-nu $isUpd -și $sbOn) { $targets += "not_updated" }                                 dacă ($tskDis) { $targets += "task_disabled" }                                 dacă (-nu $isUpd -și ($tskDis -sau (Test-ConfidenceLevel $conf 'TemporarPaused')) { $targets += "temp_failures" }                                 if (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -or ($tskNF -and $hasErr))) { $targets += "perm_failures" }                                 dacă (-nu $isUpd -și (Test-ConfidenceLevel $conf "ActionRequired")) { $targets += "action_required" }                                 dacă (-nu $sbOn) { $targets += "secureboot_off" }                                 if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $targets += "rollout_inprogress" }                                 pentru a căuta ($tgt în $targets) {                                     $tgtPath = Join-Path $dataDir "$tgt.json"                                     if (Test-Cale $tgtPath) {                                         $existing = Get-Content $tgtPath -Brut | ConvertFrom-Json                                         $existing = @($existing) + @([PSCustomObject]$slim)                                         $existing | ConvertTo-Json -Adâncime 5 | Set-Content $tgtPath -codificare UTF8                                     }                                 }                             }                             # Regenerare CSV-uri din JSON-uri corectate                             Write-Host " Regenerarea CSV-urilor din datele corectate..." -Prim-planColor gri                             $newTimestamp = Get-Date - Format "yyyyMMdd-HHmmss"                             pentru a căuta ($cat în $categoryFiles) {                                 $catJsonPath = Join-Path $dataDir "$cat.json"                                 $catCsvPath = Join-Path $OutputPath "SecureBoot_${cat}_$newTimestamp.csv"                                 if (Test-Path $catJsonPath) {                                     încercați {                                         $catJsonData = Get-Content $catJsonPath -Brut | ConvertFrom-Json                                         dacă ($catJsonData.Count -gt 0) {                                             $catJsonData | Export-Csv -Path $catCsvPath -NoTypeInformation -codificare UTF8                                         }                                     } captură { }                                 }                             }                             # Recount stats from the patched JSON files                             Write-Host " Recalculare rezumat din datele corectate..." -Prim planColor gri                             $patchedStats = [ordonat]@{ 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 în $categoryFiles) {                                 $catPath = Join-Path $dataDir "$cat.json"                                 $cnt = 0                                 if (Test-Path $catPath) { try { $cnt = (Get-Content $catPath -Raw | ConvertFrom-Json). Count } captură { } }                                 comutator ($cat) {                                     "updated_devices" { $pUpdated = $cnt }                                     "erori" { $pErrors = $cnt }                                     "known_issues" { $pKI = $cnt }                                     "missing_kek" { $pKEK = $cnt }                                     "not_updated" { } # calculat                                     "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). Conta                             $pTotal = $pUpdated + $pNotUpdated + $pSBOff                             Write-Host " Corecție delta terminată: $totalChanged dispozitive actualizate" -Prim planColor Verde                             Write-Host " Total: $pTotal | Actualizat: $pUpdated | Neacceptat: $pNotUpdated | Erori: $pErrors" - Culoare alb prim plan                             # Actualizare manifest                             $stManifestDir = Join-Path $OutputPath ".cache"                             $stNewManifest = @{}                             foreach ($jf în $jsonFiles) {                                 $stNewManifest[$jf. NumeComplet.ToLowerInvariant()] = @{                                     LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o";); Dimensiune = $jf. Lungime                                 }                             }                             $stManifestPath cale Save-FileManifest -Manifest $stNewManifest                             Write-Host " Terminat în $([matematică]::Round($streamSw.Elapsed.TotalSeconds, 1))s (corecție delta - dispozitive $totalChanged)" -Prim-planColor verde                             # Fall through to full streaming reprocess to regenerate HTML dashboard                             # Fișierele de date sunt corectate deja, astfel încât acest lucru asigură că tabloul de bord rămâne la zi                             Write-Host " Se regenerează tabloul de bord din datele corectate..." -Culoare prim plan galben                         } altfel, {                             Write-Host " $changePct% fișiere modificate (>= 10%) - sunt necesare reprocesări complete de redare în flux" -Culoare prim plan galben                         }                     }                 }             }         }         # Creați subdirector de date pentru fișierele JSON ale dispozitivului la cerere         $dataDir = Join-Path $OutputPath "date"         dacă (-nu (test-cale $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null }         # Deduplication prin HashSet (O(1) per căutare, ~ 50 MB pentru 600K hostnames)         $seenHostnames = [System.Collections.Generic.HashSet[string]]:new([System.StringComparer]::OrdinalIgnoreCase)         # Contoare de rezumat ușoare (înlocuiește $allDevices + $uniqueDevices în memorie)         $c = @{             Total = 0; SBEnabled = 0; SBOff = 0             Actualizat = 0; HighConf = 0; Suboburi = 0; ActionReq = 0; TempPaused = 0; Neacceptat = 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             Actualizare în așteptare = 0         }         # Urmărire bucket pentru AtRisk/SafeList (seturi ușoare)         $stFailedBuckets = [System.Collections.Generic.HashSet[string]]:new()         $stSuccessBuckets = [System.Collections.Generic.HashSet[string]]:new()         $stAllBuckets = @{}         $stMfrCounts = @{}         $stErrorCodeCounts = @{}; $stErrorCodeSamples = @{}         $stKnownIssueCounts = @{}         # Batch-mode device data files: acumulează pe bucată, goli la limitele blocului         $stDeviceFiles = @("erori", "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 în $stDeviceFiles) {             $dfPath = Join-Path $dataDir "$dfName.json"             [System.IO.File]::WriteAllText($dfPath, "['n", [System.Text.Encoding]::UTF8)             $stDeviceFilePaths[$dfName] = $dfPath; $stDeviceFileCounts[$dfName] = 0         }         # Slim device record for JSON output (only essential fields, ~200 bytes vs ~2KB full)         funcție Get-SlimDevice {             param($Dev)             returnare [comandat]@{                 HostName = $Dev.HostName                 WMI_Manufacturer = dacă ($Dev.PSObject.Properties['WMI_Manufacturer']) { $Dev.WMI_Manufacturer } altceva { "" }                 WMI_Model = dacă ($Dev.PSObject.Properties['WMI_Model']) { $Dev.WMI_Model } else { "" }                 BucketId = dacă ($Dev.PSObject.Properties['BucketId']) { $Dev.BucketId } else { "" }                 ConfidenceLevel = dacă ($Dev.PSObject.Properties['ConfidenceLevel']) { $Dev.ConfidenceLevel } else { "" }                 IsUpdated = $Dev.IsUpdated                 UEFICA2023Error = if ($Dev.PSObject.Properties['UEFICA2023Error']) { $Dev.UEFICA2023Error } else { $null }                 SecureBootTaskStatus = dacă ($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 }             }         }         # Goliți lotul la fișierul JSON (modul de adăugare)         funcție Flush-DeviceBatch {             param([șir]$StreamName, [System.Collections.Generic.List[obiect]]$Batch)             dacă ($Batch.Count -eq 0) { return }             $fPath = $stDeviceFilePaths[$StreamName]             $fSb = [System.Text.StringBuilder]::new()             pentru fiecare ($fDev din $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)         }         # BUCLĂ DE REDARE ÎN FLUX PRINCIPALĂ         $stChunkSize = dacă ($totalFiles -le 10000) { $totalFiles } altfel { 10000 }         $stTotalChunks = [matematică]::Ceiling($totalFiles / $stChunkSize)         $stPeakMemMB = 0         dacă ($stTotalChunks -gt 1) {             Write-Host "Se procesează fișierele $totalFiles în bucăți $stTotalChunks de $stChunkSize (redare în flux, $ParallelThreads fire):" -Prim-planColor Cyan         } altfel, {             Write-Host "Se procesează fișierele $totalFiles (redare în flux, $ParallelThreads fire):" -Prim-planColor Cyan         }         pentru ($ci = 0; $ci -lt $stTotalChunks; $ci++) {             $cStart = $ci * $stChunkSize             $cEnd = [matematică]::Min($cStart + $stChunkSize, $totalFiles) - 1             $cFiles = $jsonFiles[$cStart.) $cEnd]             dacă ($stTotalChunks -gt 1) {                 Write-Host " Bloc $($ci + 1)/$stTotalChunks fișiere ($($cFiles.Count): " -NoNewline -Prim-planColor gri             } altfel, {                 Write-Host " Se încarcă fișierele $($cFiles.Count): " -NoNewline -Prim-planColor gri             }             $cSw = [System.Diagnostics.Stopwatch]::StartNew()             $rawDevices = Load-FilesParallel -Files $cFiles -Fire $ParallelThreads             # Liste pe fiecare bucată de lot             $cBatches = @{}             foreach ($df în $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]]:new() }             $cNew = 0; $cDupe = 0             pentru a căuta ($raw în $rawDevices) {                 dacă (-nu $raw) { continue }                 $device = Normalize-DeviceRecord $raw                 $hostname = $device. Hostname                 dacă (-nu $hostname) { continue }                 dacă ($seenHostnames.Contains($hostname)) { $cDupe++; continue }                 [nul]$seenHostnames.Add($hostname)                 $cNew++; $c.Total++                 $sbOn = ($device. SecureBootEnabled -ne $false -and "$($device. SecureBootEnabled)" -ne "False")                 dacă ($sbOn) { $c.SBEnabled++ } altceva { $c.SBOff++; $cBatches["secureboot_off"]. Add((Get-SlimDevice $device)) }                 $isUpd = $device. IsUpdated -eq $true                 $conf = dacă ($device. PSObject.Properties['ConfidenceLevel'] -and $device. ConfidenceLevel) { "$($device. ConfidenceLevel)" } altcineva { "" }                 $hasErr = (-nu [șir]::IsNullOrEmpty($device. UEFICA2023Error) -și "$($device. UEFICA2023Error)" -ne "0" -and "$($device. UEFICA2023Error)" -ne "")                 $tskDis = ($device. SecureBootTaskEnabled -eq $false -or "$($device. SecureBootTaskStatus)" -eq 'Disabled' -sau "$($device. SecureBootTaskStatus)" -eq "NotFound")                 $tskNF = ("$($device. SecureBootTaskStatus)" -eq "NotFound")                 $bid = dacă ($device. PSObject.Properties['BucketId'] -and $device. BucketId) { "$($device. BucketId)" } altfel { "" }                 $e 1808 = dacă ($device. PSObject.Properties['Event1808Count']) { [int]$device. Eveniment1808Count } altceva { 0 }                 $e 1801 = dacă ($device. PSObject.Properties['Event1801Count']) { [int]$device. Eveniment1801Count } altceva { 0 }                 $e 1803 = dacă ($device. PSObject.Properties['Event1803Count']) { [int]$device. Eveniment1803Count } altfel { 0 }                 $mKEK = ($e 1803 -gt 0 -sau $device. MissingKEK -eq $true -or "$($device. MissingKEK)" -eq "True")                 $hKI = ((nu [șir]::IsNullOrEmpty($device. SkipReasonKnownIssue)) -sau (-nu [șir]::IsNullOrEmpty($device. KnownIssueId)))                 $rStat = dacă ($device. PSObject.Properties['RolloutStatus']) { $device. RolloutStatus } else { "" }                 $mfr = dacă ($device. PSObject.Properties['WMI_Manufacturer'] -and -not [string]::IsNullOrEmpty($device. WMI_Manufacturer)) { $device. WMI_Manufacturer } altceva { "Necunoscut" }                 $bid = dacă (-nu [șir]::IsNullOrEmpty($bid)) { $bid } altfel{ "" }                 # Semnalizare în așteptare actualizare pre-calculare (politică/WinCS aplicată, stare neactivată încă, SB ON, activitate neactivată)                 $uefiStatus = dacă ($device. PSObject.Properties['UEFICA2023Status']) { "$($device. UEFICA2023Status)" } altcineva { "" }                 $hasPolicy = ($device. PSObject.Properties['AvailableUpdatesPolicy'] -and $null -ne $device. AvailableUpdatesPolicy și "$($device. AvailableUpdatesPolicy)" -ne '')                 $hasWinCS = ($device. PSObject.Properties['WinCSKeyApplied'] -and $device. WinCSKeyApplied -eq $true)                 $statusPending = ([șir]::IsNullOrEmpty($uefiStatus) -sau $uefiStatus -eq 'NotStarted' -sau $uefiStatus -eq 'InProgress')                 $isUpdatePending = (($hasPolicy -sau $hasWinCS) - și $statusPending -și -not $isUpd -și $sbOn -și -not $tskDis)                 dacă ($isUpd) {                     $c.Updated++; [nul]$stSuccessBuckets.Add($bid); $cBatches["updated_devices"]. Add((Get-SlimDevice $device))                     # Urmăriți dispozitivele actualizate care necesită repornire (UEFICA2023Status=Actualizat, dar Evenimentul1808=0)                     if ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) }                 }                 elseif (-nu $sbOn) {                     # SecureBoot OFF - în afara domeniului de aplicare, nu clasifica de încredere                 }                 altfel, {                     if ($isUpdatePending) { } # Counted separat in Update Pending — mutual exclusive for pie chart                     elseif (Test-ConfidenceLevel $conf "HighConfidence") { $c.HighConf++ }                     elseif (Test-ConfidenceLevel $conf "UnderObservation") { $c.UnderObs++ }                     elseif (Test-ConfidenceLevel $conf "TemporarPaused") { $c.TempPaused++ }                     elseif (Test-ConfidenceLevel $conf "Neacceptat") { $c.NotSupported++ }                     altfel, { $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++ }                 dacă ($hasErr) {                     $c.WithErrors++; [nul]$stFailedBuckets.Add($bid); $cBatches["erori"]. Add((Get-SlimDevice $device))                     $ec = $device. UEFICA2023Eroare                     if (-not $stErrorCodeCounts.ContainsKey($ec)) { $stErrorCodeCounts[$ec] = 0; $stErrorCodeSamples[$ec] = @() }                     $stErrorCodeCounts[$ec]++                     if ($stErrorCodeSamples[$ec]. Count -lt 5) { $stErrorCodeSamples[$ec] += $hostname }                 }                 dacă ($hKI) {                     $c.WithKnownIssues++; $cBatches["known_issues"]. Add((Get-SlimDevice $device))                     $ki = dacă (-nu [șir]::IsNullOrEmpty($device. SkipReasonKnownIssue)) { $device. SkipReasonKnownIssue } altceva, { $device. KnownIssueId }                     dacă (-nu $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 'TemporarPaused')) { $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++ }                 # Actualizare în așteptare: politică sau WinCS aplicat, stare în așteptare, SB ACTIVAT, activitate ne dezactivată                 dacă ($isUpdatePending) {                     $c.UpdatePending++; $cBatches["update_pending"]. Add((Get-SlimDevice $device))                 }                 dacă (-nu $isUpd -și $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) }                 # Sub Dispozitive de observație (separat de Acțiune necesară)                 dacă (-nu $isUpd -și (Test-ConfidenceLevel $conf 'UnderObservation')) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) }                 # Acțiune obligatoriu: ne-actualizat, SB ACTIVAT, care nu se potrivește cu alte categorii de încredere, nu Actualizare în așteptare                 dacă (-nu $isUpd -și $sbOn -și -not $isUpdatePending -and -not (Test-ConfidenceLevel $conf 'HighConfidence') -and -not (Test-ConfidenceLevel $conf 'UnderObservation') -and -not (Test-ConfidenceLevel $conf 'TemporarPaused') -and -not (Test-ConfidenceLevel $conf 'NotSupported')) {                     $cBatches["action_required"]. Add((Get-SlimDevice $device))                 }                 dacă (-nu $stMfrCounts.ContainsKey($mfr)) { $stMfrCounts[$mfr] = @{ Total=0; Actualizat=0; Actualizare în așteptare=0; HighConf=0; UnderObs=0; ActionReq=0; TempPaused=0; NotSupported=0; SBOff=0; CuErrors=0 } }                 $stMfrCounts[$mfr]. Total++                 dacă ($isUpd) { $stMfrCounts[$mfr]. Actualizat++ }                 elseif (-nu $sbOn) { $stMfrCounts[$mfr]. SBOff++ }                 elseif ($isUpdatePending) { $stMfrCounts[$mfr]. Actualizare în așteptare++ }                 elseif (Test-ConfidenceLevel $conf "HighConfidence") { $stMfrCounts[$mfr]. HighConf++ }                 elseif (Test-ConfidenceLevel $conf "SubObservare") { $stMfrCounts[$mfr]. UnderObs++ }                 elseif (Test-ConfidenceLevel $conf "TemporarPaused") { $stMfrCounts[$mfr]. TempPaused++ }                 elseif (Test-ConfidenceLevel $conf "Neacceptat") { $stMfrCounts[$mfr]. NotSupported++ }                 altfel, { $stMfrCounts[$mfr]. ActionReq++ }                 dacă ($hasErr) { $stMfrCounts[$mfr]. CuErrors++ }                 # Urmăriți toate dispozitivele în funcție de bucket (inclusiv BucketId gol)                 $bucketKey = dacă ($bid -și $bid -ne "") { $bid } else { "(empty)" }                 if (-not $stAllBuckets.ContainsKey($bucketKey)) {                     $stAllBuckets[$bucketKey] = @{ Count=0; Actualizat=0; Manufacturer=$mfr; Model=""; BIOS="" }                     dacă ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. Model = $device. WMI_Model }                     dacă ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription }                 }                 $stAllBuckets[$bucketKey]. Count++                 dacă ($isUpd) { $stAllBuckets[$bucketKey]. Actualizat++ }             }             # Goliți loturile pe disc             foreach ($df în $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] }             $rawDevices = $null; $cBatches = $null; [System.GC]::Collect()             $cSw.Stop()             $cTime = [Matematică]:Round($cSw.Elapsed.TotalSeconds, 1)             $cRem = $stTotalChunks - $ci - 1             $cEta = dacă ($cRem -gt 0) { " | ETA: ~$([Matematică]::Round($cRem * $cSw.Elapsed.TotalSeconds / 60, 1)) min" } else { "" }             $cMem = [matematică]::Round([System.GC]::GetTotalMemory($false) / 1MB, 0)             dacă ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem }             Write-Host " +$cNew forme noi, $cDupe, ${cTime}s | Mem: ${cMem}MB$cEta" -Prim planColor Verde         }         # Finalizare matrice JSON         foreach ($dfName în $stDeviceFiles) {             [System.IO.File]::AppendAllText($stDeviceFilePaths[$dfName], "'n]", [System.Text.Encoding]::UTF8)             Write-Host " $dfName.json: dispozitive $($stDeviceFileCounts[$dfName]) " -Prim-planColor DarkGray         }         # Statistici derivate         $stAtRisk = 0; $stSafeList = 0         pentru a căuta ($bid în $stAllBuckets.Keys) {             $b = $stAllBuckets[$bid]; $nu = $b.Count - $b.Actualizat             dacă ($stFailedBuckets.Contains($bid)) { $stAtRisk += $nu }             elseif ($stSuccessBuckets.Contains($bid)) { $stSafeList += $nu }         }         $stAtRisk = [matematică]:Max(0, $stAtRisk - $c.WithErrors)         # NotUptodate = numărul din not_updated lot (dispozitive cu SB ACTIVAT și neactualizate)         $stNotUptodate = $stDeviceFileCounts["not_updated"]         $stats = [ordonat]@{             ReportGeneratedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss")             TotalDevices = $c.Total; SecureBootEnabled = $c.SBEnabled; SecureBootOFF = $c.SBOff             Actualizat = $c.Updated; HighConfidence = $c.HighConf; UnderObservation = $c.UnderObs             ActionRequired = $c.ActionReq; TemporarPaused = $c.TempPaused; NotSupported = $c.NotSupported             NoConfidenceData = $c.NoConfData; TaskDisabled = $c.TaskDisabled; TaskNotFound = $c.TaskNotFound             TaskDisabledNotUpdated = $c.TaskDisabledNotUpdated             CertificateUpdated = $c.Updated; NotUptodate = $stNotUptodate; CompletUpdated = $c.Updated             Actualizări în așteptare = $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; Actualizare în așteptare = $c.UpdatePending             AtRiskDevices = $stAtRisk; SafeListDevices = $stSafeList             PercentWithErrors = dacă ($c.Total -gt 0) { [matematică]:Round(($c.WithErrors/$c.Total)*100,2) } altfel { 0 }             PercentAtRisk = dacă ($c.Total -gt 0) { [matematică]:Round(($stAtRisk/$c.Total)*100,2) } altceva { 0 }             PercentSafeList = dacă ($c.Total -gt 0) { [matematică]:Round(($stSafeList/$c.Total)*100,2) } altceva { 0 }             PercentHighConfidence = dacă ($c.Total -gt 0) { [matematică]:Round(($c.HighConf/$c.Total)*100,1) } altfel { 0 }             PercentCertUpdated = if ($c.Total -gt 0) { [math]:Round(($c.Updated/$c.Total)*100,1) } else { 0 }             PercentActionRequired = dacă ($c.Total -gt 0) { [matematică]:Round(($c.ActionReq/$c.Total)*100,1) } altceva { 0 }             PercentNotUptodate = dacă ($c.Total -gt 0) { [matematică]:Round($stNotUptodate/$c.Total*100;1) } altceva { 0 }             PercentFullyUpdated = dacă ($c.Total -gt 0) { [matematică]:Round(($c.Updated/$c.Total)*100,1) } altceva { 0 }             UniqueBuckets = $stAllBuckets.Count; PeakMemoryMB = $stPeakMemMB; ProcessingMode = "Redare în flux"         }         # Scrie CSVs         [PSCustomObject]$stats | Export-Csv -path ($OutputPath "SecureBoot_Summary_$timestamp.csv") -NoTypeInformation -codificare UTF8         $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descendent | ForEach-Object {             [PSCustomObject]@{ Manufacturer=$_. Cheie; Contor=$_. Valoare.Total; Actualizat=$_. Value.Updated; HighConfidence=$_. Value.HighConf; ActionRequired=$_. Value.ActionReq }         } | Export-Csv -Path (cale de asociere $OutputPath "SecureBoot_ByManufacturer_$timestamp.csv") -NoTypeInformation -codificare UTF8         $stErrorCodeCounts.GetEnumerator() | valoare Sort-Object - descendentă | ForEach-Object {             [PSCustomObject]@{ ErrorCode=$_. Cheie; Contor=$_. Valoarea; SampleDevices=($stErrorCodeSamples[$_. Cheie] -unire ", ") }         } | Export-Csv -Path ($OutputPath "SecureBoot_ErrorCodes_$timestamp.csv") -NoTypeInformation -codificare UTF8         $stAllBuckets.GetEnumerator() | Sort-Object { $_. Value.Count } -Descendent | ForEach-Object {             [PSCustomObject]@{ BucketId=$_. Cheie; Contor=$_. Value.Count; Actualizat=$_. Value.Updated; NotUpdated=$_. Value.Count-$_. Value.Updated; Producător=$_. Value.Manufacturer }         } | Export-Csv -Path ($OutputPath "SecureBoot_UniqueBuckets_$timestamp.csv") -NoTypeInformation -codificare UTF8         # Generați CSV-uri compatibile cu orchestratorii (nume de fișiere așteptate pentru Start-SecureBootRolloutOrchestrator.ps1)         $notUpdatedJsonPath = Join-Path $dataDir "not_updated.json"         if (Test-Cale $notUpdatedJsonPath) {             încercați {                 $nuData = Get-Content $notUpdatedJsonPath -Brut | ConvertFrom-Json                 dacă ($nuData.Count -gt 0) {                     # NotUptodate CSV - orchestrator căutări pentru *NotUptodate*.csv                     $nuData | Export-Csv -Path (cale de asociere $OutputPath "SecureBoot_NotUptodate_$timestamp.csv") -NoTypeInformation -codificare UTF8                     Write-Host "Orchestrator CSV: dispozitive SecureBoot_NotUptodate_$timestamp.csv ($($nuData.Count)" -Prim planColor Gray                 }             } captură { }         }         # Scrieți date JSON pentru tabloul de bord         $stats | ConvertTo-Json -Adâncime 3 | Set-Content ($dataDir de asociere "summary.json") - codificare UTF8         # ISTORIC TRACKING: Save data point for trend chart         # Utilizați o locație de cache stabilă, astfel încât datele tendințelor să persiste în folderele de agregare temporală.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   # Dacă OutputPath arată ca "...\Aggregation_yyyyMMdd_HHmmss", memoria cache ajunge în folderul părinte.# În caz contrar, memoria cache intră chiar în OutputPath.$parentDir = Split-Path $OutputPath -Părinte         $leafName = frunză Split-Path $OutputPath         dacă ($leafName -match '^Aggregation_\d{8}' -sau $leafName -eq 'Aggregation_Current') {             # Orchestrator creat timestamped folder - utilizarea părinte pentru memoria cache stabilă             $historyPath = Join-Path $parentDir ".cache\trend_history.json"         } altfel, {             $historyPath = Join-Path $OutputPath ".cache\trend_history.json"         }         $historyDir = Split-Path $historyPath -Părinte         if (-not (Test-Path $historyDir)) { New-Item -ItemType Directory -Path $historyDir -Force | Out-Null }         $historyData = @()         if (Test-Cale $historyPath) {             încercați { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } captură { $historyData = @() }         }         # De asemenea, verificați în Interiorul OutputPath\.cache\ (locație moștenită din versiuni mai vechi)         # Îmbinați punctele de date care nu se află deja în istoricul principal         dacă ($leafName -eq 'Aggregation_Current' sau $leafName -match '^Aggregation_\d{8}') {             $innerHistoryPath = Join-Path $OutputPath ".cache\trend_history.json"             if ((Test-Path $innerHistoryPath) -and $innerHistoryPath -ne $historyPath) {                 încercați {                     $innerData = @(Get-Content $innerHistoryPath -Raw | ConvertFrom-Json)                     $existingDates = @($historyData | ForEach-Object { $_. Data })                     foreach ($entry în $innerData) {                         dacă ($entry. Data și $entry. Data -notin $existingDates) {                             $historyData += $entry                         }                     }                     dacă ($innerData.Count -gt 0) {                         Write-Host " Puncte de date $($innerData.Count) îmbinate din memoria cache internă" -Prim-planColor DarkGray                     }                 } captură { }             }         }

# BOOTSTRAP: Dacă istoricul tendințelor este gol/rare, reconstruiți din datele istorice         dacă ($historyData.Count -lt 2 -and ($leafName -match '^Aggregation_\d{8}' -sau $leafName -eq 'Aggregation_Current')) {             Write-Host " Bootstrapping trend history from historical data..." -ForegroundColor Yellow             $dailyData = @{}                          # Sursa 1: Rezumat CSV-uri în interiorul folderului curent (Aggregation_Current păstrează toate CSV-uri rezumat)             $localSummaries = Get-ChildItem $OutputPath -Filtru "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Nume Sort-Object             pentru a căuta ($summCsv în $localSummaries) {                 încercați {                     $summ = Import-Csv $summCsv.NumeComplet | Select-Object -Primul 1                     dacă ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0 -și $summ. ReportGeneratedAt) {                         $dateStr = ([dată-oră]$summ. ReportGeneratedAt). ToString("yyyy-MM-dd")                         $updated = dacă ($summ. Actualizat) { [int]$summ. S-a actualizat } altcineva { 0 }                         $notUpd = dacă ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated }                         $dailyData[$dateStr] = [PSCustomObject]@{                             Data = $dateStr; Total = [int]$summ. Dispozitive Total; Actualizat = $updated; Neacceptat = $notUpd                             NeedsReboot = 0; Erori = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } altceva { 0 }                         }                     }                 } captură { }             }                          # Sursa 2: Timestamped vechi Aggregation_ * foldere (legacy, în cazul în care încă mai există)             $aggFolders = Get-ChildItem $parentDir -Director - Filtru "Aggregation_*" -EA SilentlyContinue |                 Where-Object { $_. Nume -potrivire '^Aggregation_\d{8}' } |                 Nume Sort-Object             pentru a căuta ($folder în $aggFolders) {                 $summCsv = Get-ChildItem $folder. FullName -Filtru "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Select-Object -Primul 1                 dacă ($summCsv) {                     încercați {                         $summ = Import-Csv $summCsv.NumeComplet | Select-Object -Primul 1                         dacă ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0) {                             $dateStr = $folder. Nume -replace '^Aggregation_(\d{4})(\d{2})(\d{2})_.*', '$1-$2-$3'                             $updated = dacă ($summ. Actualizat) { [int]$summ. S-a actualizat } altcineva { 0 }                             $notUpd = dacă ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated }                             $dailyData[$dateStr] = [PSCustomObject]@{                                 Data = $dateStr; Total = [int]$summ. Dispozitive Total; Actualizat = $updated; NotUpdated = $notUpd                                 NeedsReboot = 0; Erori = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } altceva { 0 }                             }                         }                     } captură { }                 }             }                          # Sursa 3: RolloutState.json WaveHistory (are pe val timestamps din ziua 1)             # Acest lucru oferă puncte de date de referință chiar și atunci când nu există foldere de agregare vechi             $rolloutStatePaths = @(                 (Join-Path $parentDir "RolloutState\RolloutState.json"),                 (Cale de asociere $OutputPath "RolloutState\RolloutState.json")             )             pentru a căuta ($rsPath în $rolloutStatePaths) {                 if (Test-Path $rsPath) {                     încercați {                         $rsData = Get-Content $rsPath -Brut | ConvertFrom-Json                         if ($rsData.WaveHistory) {                             # Utilizați datele de început ale valului ca puncte de date de tendință                             # Calculați dispozitivele cumulative orientate spre fiecare val                             $cumulativeTargeted = 0                             foreach ($wave în $rsData.WaveHistory) {                                 dacă ($wave. StartedAt -și $wave. DeviceCount) {                                     $waveDate = ([dată-oră]$wave. StartedAt). ToString("yyyy-MM-dd")                                     $cumulativeTargeted += [int]$wave. Cont dispozitiv                                     if (-not $dailyData.ContainsKey($waveDate)) {                                         # Aproximative: la ora de începere a valului, numai dispozitivele din undele anterioare au fost actualizate                                         $dailyData[$waveDate] = [PSCustomObject]@{                                             Data = $waveDate; Total = $c.Total; Actualizat = [matematic]:Max(0, $cumulativeTargeted - [int]$wave. DeviceCount)                                             NotUpdated = $c.Total - [matematică]:Max(0, $cumulativeTargeted - [int]$wave. DeviceCount)                                             NeedsReboot = 0; Erori = 0; AcțiuneObligatoriu = 0                                         }                                     }                                 }                             }                         }                     } captură { }                     break # Utilizați primul găsit                 }             }

dacă ($dailyData.Count -gt 0) {                 $historyData = @($dailyData.GetEnumerator() | Sort-Object cheie | ForEach-Object { $_. Valoare })                 Write-Host punctele de date "Bootstrapped $($historyData.Count) din rezumatele istorice" -ForegroundColor Green             }         }

# Adăugați punctul de date curent (deduplicați după zi - păstrați cele mai recente date pe zi)         $todayKey = (Get-Date). ToString("yyyy-MM-dd")         $existingToday = $historyData | Where-Object { "$($_. Dată)" - cum ar fi "$todayKey*" }         dacă ($existingToday) {             # Înlocuiți intrarea de astăzi             $historyData = @($historyData | Where-Object { "$($_. Dată)" -notlike "$todayKey*" })         }         $historyData += [PSCustomObject]@{             Dată = $todayKey             Total = $c.Total             Actualizat = $c.Updated             NotUpdated = $stNotUptodate             NeedsReboot = $c.NeedsReboot             Erori = $c.WithErrors             ActionRequired = $c.ActionReq         }         # Eliminați punctele de date eronate (0 în total) și păstrați ultimele 90         $historyData = @($historyData | Where-Object { [int]$_. Total -gt 0 })         # Fără capac - datele tendinței sunt ~100 byți/intrare, un an întreg = ~36 KB         $historyData | ConvertTo-Json -Adâncime 3 | Set-Content $historyPath -codificare UTF8         Write-Host " Istoric tendințe: puncte de date $($historyData.Count) " -Prim planColor ÎntunecatGray                  # Generare date diagramă de tendință pentru HTML         $trendLabels = ($historyData | ForEach-Object { "'$($_. Dată)'" }) -asociere ","         $trendUpdated = ($historyData | ForEach-Object { $_. Actualizat }) -join ","         $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) -join ","         $trendTotal = ($historyData | ForEach-Object { $_. Total }) -asociere ","         # Proiectare: extinde linia de tendință utilizând dublarea exponențială (2,4,8,16...)         # Derivă dimensiunea valului și perioada de observație din datele reale ale istoricului tendințelor.# - Wave dimensiunea = cea mai mare creștere o singură perioadă văzută în istorie (cel mai recent val implementat)         # - Zile de observare = medie zile calendaristice între punctele de date de tendință (cât de des rulăm)         # Apoi dublează dimensiunea undelor în fiecare perioadă, potrivind strategia de creștere 2x a orchestratorului.$projLabels = ""; $projUpdated = ""; $projNotUpdated = ""; $hasProjection = $false         dacă ($historyData.Count -ge 2) {             $lastUpdated = $c.Actualizat             $remaining = $stNotUptodate # Numai dispozitivele ne actualizate SB-ON (exclude SecureBoot OFF)             $projDates = @(); $projValues = @(); $projNotUpdValues = @()             $projDate = Get-Date

# Derivați dimensiunea valului și perioada de observație din istoricul tendințelor             $increments = @()             $dayGaps = @()             pentru ($hi = 1; $hi -lt $historyData.Count; $hi++) {                 $inc = $historyData[$hi]. Actualizat - $historyData[$hi-1]. Actualizat                 dacă ($inc -gt 0) { $increments += $inc }                 încercați {                     $d 1 = [dată-oră]::P arse($historyData[$hi-1]. Data)                     $d 2 = [dată-oră]::P arse($historyData[$hi]. Data)                     $gap = ($d 2 - $d 1). TotalDays                     dacă ($gap -gt 0) { $dayGaps += $gap }                 } captură {}             }             # Dimensiune val = cea mai recentă incrementare pozitivă (val curent), revenire la medie, minim 2             $waveSize = dacă ($increments. Count -gt 0) {                 [matematică]:Max(2, $increments[-1])             } altfel , { 2 }             # Perioada de observație = diferența medie dintre punctele de date (zile calendaristice pe undă), minim 1             $waveDays = dacă ($dayGaps.Count -gt 0) {                 [matematică]::Max(1, [matematică]:Round(($dayGaps | Measure-Object -Average). Medie, 0))             } altfel , { 1 }

            Write-Host " Proiecție: waveSize=$waveSize (de la ultima incrementare), waveDays=$waveDays (spațiu liber mediu din istorie)" -Prim-planColor DarkGray

$dayCounter = 0             # Project până când toate dispozitivele sunt actualizate sau maximum 365 de zile             pentru ($pi = 1; $pi -le 365; $pi++) {                 $projDate = $projDate.AddDays(1)                 $dayCounter++                 # La fiecare limită a perioadei de observație, implementați un val apoi dublu                 dacă ($dayCounter -ge $waveDays) {                     $devicesThisWave = [matematică]::Min($waveSize, $remaining)                     $lastUpdated += $devicesThisWave                     $remaining -= $devicesThisWave                     if ($lastUpdated -gt ($c.Updated + $stNotUptodate)) { $lastUpdated = $c.Updated + $stNotUptodate; $remaining = 0 }                     # Dimensiune undă dublă pentru perioada următoare (orchestrator strategie 2x)                     $waveSize = $waveSize * 2                     $dayCounter = 0                 }                 $projDates += "'$($projDate.ToString("yyyy-MM-dd")")'"                 $projValues += $lastUpdated                 $projNotUpdValues += [matematică]:Max(0, $remaining)                 dacă ($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 " Proiecție: trebuie cel puțin 2 puncte de date de tendință pentru a deriva temporizarea valului" -Prim-planColor DarkGray         }         # Construirea șirurilor de date de diagramă combinate pentru șirul de aici         $allChartLabels = dacă ($hasProjection) { "$trendLabels,$projLabels" } altceva { $trendLabels }         $projDataJS = dacă ($hasProjection) { $projUpdated } altfel { "" }         $projNotUpdJS = dacă ($hasProjection) { $projNotUpdated } altfel { "" }         $histCount = ($historyData | Measure-Object). Conta         $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descendent | ForEach-Object {             @{ name=$_. Cheie; total=$_. Valoare.Total; actualizat=$_. Value.Updated; highConf=$_. Value.HighConf; actionReq=$_. Value.ActionReq }         } | ConvertTo-Json -Adâncime 3 | Set-Content ($dataDir "manufacturers.json de asociere") - codificare UTF8         # Convertiți fișierele de date JSON în CSV pentru descărcări Excel care pot fi citite de oameni         Write-Host "Conversia datelor dispozitivului în CSV pentru descărcare Excel..." -Prim planColor Gri         pentru a căuta ($dfName în $stDeviceFiles) {             $jsonFile = Join-Path $dataDir "$dfName.json"             $csvFile = Join-Path $OutputPath "SecureBoot_${dfName}_$timestamp.csv"             if (Test-Path $jsonFile) {                 încercați {                     $jsonData = Get-Content $jsonFile -Brut | ConvertFrom-Json                     dacă ($jsonData.Count -gt 0) {                         # Includeți coloane suplimentare pentru update_pending CSV                         $selectProps = dacă ($dfName -eq "update_pending") {                             @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Status', 'UEFICA2023Error', 'AvailableUpdatesPolicy', 'WinCSKeyApplied', 'SecureBootTaskStatus')                         } altfel, {                             @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Error', 'SecureBootTaskStatus', 'KnownIssueId', 'SkipReasonKnownIssue')                         }                         $jsonData | Select-Object $selectProps |                             Export-Csv -Path $csvFile -NoTypeInformation -codificare UTF8                         Write-Host " $dfName -> rânduri $($jsonData.Count) -> CSV" -Prim-planColor DarkGray                     }                 } captură { Write-Host " $dfName - ignorat" -Prim-planColor DarkYellow }             }         }         # Generați tabloul de bord HTML auto-conținut         $htmlPath = Join-Path $OutputPath "timestamp.html SecureBoot_Dashboard_$         Write-Host "Se generează tabloul de bord HTML auto-conținut..." -Prim-planColor galben         # VELOCITY PROJECTION: Calculați din istoricul de scanare sau rezumatul anterior         $stDeadline = [datetime]"2026-06-24" # KEK cert expiră         $stDaysToDeadline = [matematică]:Max(0; ($stDeadline - (Get-Date)). Zile)         $stDevicesPerDay = 0         $stProjectedDate = $null         $stVelocitySource = "N/A"         $stWorkingDays = 0         $stCalendarDays = 0         # Încercați mai întâi istoricul tendințelor (ușor, întreținut deja de agregator - înlocuiește ScanHistory.json umflate)         dacă ($historyData.Count -ge 2) {             $validHistory = @($historyData | Where-Object { [int]$_. Total -gt 0 -and [int]$_. Actualizat -ge 0 })             dacă ($validHistory.Count -ge 2) {                 $prev = $validHistory[-2]; $curr = $validHistory[-1]                 $prevDate = [dată-oră]::P arse($prev. Date.Substring(0, [Matematică]:Min(10, $prev. Date.Length)))                 $currDate = [dată-oră]::P arse($curr. Date.Substring(0, [Matematică]:Min(10, $curr. Date.Length)))                 $daysDiff = ($currDate - $prevDate). TotalDays                 dacă ($daysDiff -gt 0) {                     $updDiff = [int]$curr. Actualizat - [int]$prev. Actualizat                     dacă ($updDiff -gt 0) {                         $stDevicesPerDay = [matematică]:Round($updDiff / $daysDiff, 0)                         $stVelocitySource = "TrendHistory"                     }                 }             }         }         # Încercați rezumatul lansării orchestratorului (are o viteză pre-calculată)         if ($stVelocitySource -eq "N/A" -and $RolloutSummaryPath -and (Test-Path $RolloutSummaryPath)) {             încercați {                 $rolloutSummary = Get-Content $RolloutSummaryPath -Brut | ConvertFrom-Json                 if ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) {                     $stDevicesPerDay = [matematică]::Round([dublu]$rolloutSummary.DevicesPerDay, 1)                     $stVelocitySource = "Orchestrator"                     if ($rolloutSummary.ProjectedCompletionDate) {                         $stProjectedDate = $rolloutSummary.ProjectedCompletionDate                     }                     if ($rolloutSummary.WorkingDaysRemaining) { $stWorkingDays = [int]$rolloutSummary.WorkingDaysRemaining }                     if ($rolloutSummary.CalendarDaysRemaining) { $stCalendarDays = [int]$rolloutSummary.CalendarDaysRemaining }                 }             } captură { }         }         # Rezervă: încercați rezumatul anterior CSV (căutare folderul curent ȘI folderele de agregare părinte/frate)         dacă ($stVelocitySource -eq "N/A") {             $searchPaths = @(                 (Cale de asociere $OutputPath "SecureBoot_Summary_*.csv")             )             # De asemenea, căutare foldere de agregare frate (orchestrator creează folder nou la fiecare rulare)             $parentPath = Split-Path $OutputPath -Părinte             dacă ($parentPath) {                 $searchPaths += ($parentPath cale de asociere "Aggregation_*\SecureBoot_Summary_*.csv")                 $searchPaths += ($parentPath "SecureBoot_Summary_*.csv")             }             $prevSummary = $searchPaths | ForEach-Object { Get-ChildItem $_ -EA SilentlyContinue } | Sort-Object LastWriteTime -Descendent | Select-Object -Primul 1             dacă ($prevSummary) {                 încercați {                     $prevStats = Get-Content $prevSummary.FullName | ConvertFrom-Csv                     $prevDate = [datetime]$prevStats.ReportGeneratedAt                     $daysSinceLast = ((Get-Date) - $prevDate). TotalDays                     dacă ($daysSinceLast -gt 0,01) {                         $prevUpdated = [int]$prevStats.Updated                         $updDelta = $c.Actualizat - $prevUpdated                         dacă ($updDelta -gt 0) {                             $stDevicesPerDay = [matematică]:Round($updDelta / $daysSinceLast, 0)                             $stVelocitySource = "PreviousReport"                         }                     }                 } captură { }             }         }         # Rezervă: calcula viteza de la span istoricul de tendință complet (primul vs cel mai recent punct de date)         if ($stVelocitySource -eq "N/A" -and $historyData.Count -ge 2) {             $validHistory = @($historyData | Where-Object { [int]$_. Total -gt 0 -and [int]$_. Actualizat -ge 0 })             if ($validHistory.Count -ge 2) {                 $first = $validHistory[0]                 $last = $validHistory[-1]                 $firstDate = [dată-oră]::P arse($first. Date.Substring(0, [Matematică]::Min(10, $first. Date.Length)))                 $lastDate = [dată-oră]::P arse($last. Date.Substring(0, [Matematică]:Min(10, $last. Date.Length)))                 $daysDiff = ($lastDate - $firstDate). TotalDays                 dacă ($daysDiff -gt 0) {                     $updDiff = [int]$last. Actualizat - [int]$first. Actualizat                     dacă ($updDiff -gt 0) {                         $stDevicesPerDay = [matematică]:Round($updDiff / $daysDiff, 1)                         $stVelocitySource = "TrendHistory"                     }                 }             }         }         # Calculați proiecția utilizând dublarea exponențială (în concordanță cu diagrama de tendințe)         # Reutilizați datele de proiecție calculate deja pentru diagramă, dacă sunt disponibile         dacă ($hasProjection -și $projDates.Count -gt 0) {             # Utilizați ultima dată proiectată (când toate dispozitivele sunt actualizate)             $lastProjDateStr = $projDates[-1] -replace "'", ""             $stProjectedDate = ([dată-oră]::P arse($lastProjDateStr)). ToString("MMM dd, yyyy")             $stCalendarDays = ([dată-oră]::P arse($lastProjDateStr) - (Get-Date)). Zile             $stWorkingDays = 0             $d = Get-Date             pentru ($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 și $stNotUptodate -gt 0) {             # Rezervă: proiectarea liniară dacă nu există date exponențiale disponibile             $daysNeeded = [matematică]::Ceiling($stNotUptodate / $stDevicesPerDay)             $stProjectedDate = (Get-Date). AddDays($daysNeeded). ToString("MMM dd, yyyy")             $stWorkingDays = 0; $stCalendarDays = $daysNeeded             $d = Get-Date             pentru ($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 = dacă ($stDevicesPerDay -gt 0) {             "<div><>&#128640; Dispozitive/Zi:</strong> $($stDevicesPerDay.ToString('N0')) (sursă: $stVelocitySource)</div>" +             "<div><>&#128197 puternic; Finalizare proiectată:</strong> $stProjectedDate" +             $(if ($stProjectedDate -and [datetime]::P arse($stProjectedDate) -gt $stDeadline) { " <span style='color:#dc3545; font-weight:bold'>&#9888; PAST DEADLINE</span>" } else { " <span style='color:#28a745'>&#10003; Before deadline</span>" }) +             "</div>" +             "<div><>&#128336 puternic; Zile lucrătoare:</strong> $stWorkingDays | <zile>Calendar puternice:</strong> $stCalendarDays</div>" +             "<div style='font-size:.8em; color:#888'>Termen limită: 24 iunie 2026 (expiră certificatul KEK) | Zile rămase: $stDaysToDeadline</div>"         } altfel, {             "<div style='padding:8px; background:#fff3cd; rază bordură:4px; border-left:3px solid #ffc107'>" +             "<>&#128197 puternic; Finalizare proiectată:</strong> Date insuficiente pentru calculul vitezei.                                                                                  " +             "Rulați agregarea cel puțin de două ori cu modificările de date pentru a stabili o rată.<br/>" +             "<termen>puternic:</strong> 24 iunie 2026 (expiră certificatul KEK) | <zile>puternice rămase:</strong> $stDaysToDeadline</div>"         }                  # Numărătoarea inversă de expirare a certificatului         $certToday = Get-Date         $certKekExpiry = [dată-oră]"2026-06-24"         $certUefiExpiry = [dată-oră]"2026-06-27"         $certPcaExpiry = [dată-oră]"2026-10-19"         $daysToKek = [matematică]:Max(0; ($certKekExpiry - $certToday). Zile)         $daysToUefi = [matematică]:Max(0; ($certUefiExpiry - $certToday). Zile)         $daysToPca = [matematică]:Max(0; ($certPcaExpiry - $certToday). Zile)         $certUrgency = dacă ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } altceva { '#28a745' }                  # Ajutor: Citiți înregistrările din JSON, compilarea rezumatului bucketului + primele N rânduri de dispozitiv         $maxInlineRows = 200         funcție Build-InlineTable {             param([șir]$JsonPath, [int]$MaxRows = 200, [șir]$CsvFileName = "")             $bucketSummary = ""             $deviceRows = ""             $totalCount = 0             if (Test-Cale $JsonPath) {                 încercați {                     $data = Get-Content $JsonPath -Brut | ConvertFrom-Json                     $totalCount = $data. Conta                                          # BUCKET SUMMARY: Group by BucketId, show counts per bucket with Updated from global bucket stats                     dacă ($totalCount -gt 0) {                         $buckets = $data | Group-Object BucketId | contor Sort-Object -descendent                         $bucketSummary = "><2 h3 style='font-size:.95em; culoare:#333; margin:10px 0 5px'><3 By Hardware Bucket ($($buckets. Bucketuri count)><4 /h3>"                         $bucketSummary += "><6 div style='max-height:300px; overflow-y:auto; margin-bottom:15px'><table><thead><tr><th><5 BucketID><6 /th><th style='text-align:right'>Total</th><th style='text-align:right; color:#28a745'><actualizat /th><th style='text-align:right; color:#dc3545'></th><><1 Manufacturer><2 /th></tr></thead><întruchiparea>"                         foreach ($b în $buckets) {                             $bid = dacă ($b.Name) { $b.Name } else { "(empty)" }                             $mfr = ($b.Grup | Select-Object -Primul 1). WMI_Manufacturer                             # Obțineți numărul actualizat de statistici globale de bucket (toate dispozitivele din acest bucket din întregul set de date)                             $lookupKey = $bid                             $globalBucket = dacă ($stAllBuckets.ContainsKey($lookupKey)) { $stAllBuckets[$lookupKey] } altfel, { $null }                             $bUpdatedGlobal = dacă ($globalBucket) { $globalBucket.Updated } altceva { 0 }                             $bTotalGlobal = dacă ($globalBucket) { $globalBucket.Count } altceva { $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; culoare:#28a745; font-weight:bold'>$bUpdatedGlobal><2 /td><td style='text-align:right; culoare:#dc3545; font-weight:bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n"                         }                         $bucketSummary += "</tbody></table></div>"                     }                                          # DEVICE DETAIL: First N rows as flat list                     $slice = $data | Select-Object - Primul $MaxRows                     pentru a căuta ($d în $slice) {                         $conf = $d.ConfidenceLevel                         $confBadge = dacă ($conf -match "High") { '<span class="badge-success">High Conf><2 /span>' }                                      elseif ($conf -match "Not Sup") { '<span class="badge-danger">Not Supported><6 /span>' }                                      elseif ($conf -match "Under") { '<span class="badge-info">Under Obs><0 /span>' }                                      elseif ($conf -match "Paused") { "<span class="badge-warning">Paused><4 /span>' }                                      else { '<span class="badge badge-warning">Action Req><8 /span>' }                         $statusBadge = dacă ($d.IsUpdated) { '><00 span class="badge-success"><01 actualizat</span>' }                                        elseif ($d.UEFICA2023Error) { '><04 span class="badge badge-danger"><05 Eroare</span>' }                                        else { '><08 span class="badge badge-warning"><09><0 în așteptare /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><5 $(if($d.UEFICA2023Error){$d.UEFICA2023Error}else{'-'})$d.UEFICA2023Error}else{'-'})><36 /td><td style='font-size:.75em'><39 $($d.BucketId)><40 /td></tr><3 'n"                     }                 } captură { }             }             dacă ($totalCount -eq 0) {                 returnează "><44 div style='padding:20px; culoare:#888; font-style:italic'><45 Niciun dispozitiv din această categorie.><46 /div>"             }             $showing = [matematică]::Min($MaxRows, $totalCount)             $header = "><48 div style='margin:5px 0; font-size:.85em; color:#666'><49 Total: dispozitive $($totalCount.ToString("N0")")             dacă ($CsvFileName) { $header += " | ><50 a href='$CsvFileName' style='color:#1a237e; font-weight:bold'>&#128196; Descărcați fișierul CSV complet pentru Excel><3 /a>" }             $header += "><55 /div>"             $deviceHeader = "><57 h3 style='font-size:.95em; culoare:#333; margin:10px 0 5px'><58 Device Details (showing first $showing)><59 /h3>"             $deviceTable = "><61 div style='max-height:500px; overflow-y:auto'><tabel><thead><tr><><0 HostName><1 /th><><4 Manufacturer><5 /th><th><8 Model><9 /th><th><2 Confidence><3 /th><th><6 stare><7 .th><><0 eroare><1 /th><><4 BucketId><5 /th></tr></thead><întruchiparea><2 $deviceRows><3 /tbody></table></div>"             returnează "$header$bucketSummary$deviceHeader$deviceTable"         }                  # Construiți tabele în linie din fișierele JSON aflate deja pe disc, conectându-vă la CSV-uri         $tblErrors = Build-InlineTable -JsonPath ($dataDir "errors.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_errors_$timestamp.csv"         $tblKI = Build-InlineTable -JsonPath ($dataDir "known_issues.json de asociere") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_known_issues_$timestamp.csv"         $tblKEK = Build-InlineTable -JsonPath ($dataDir "missing_kek.json pentru unire") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_missing_kek_$timestamp.csv"         $tblNotUpd = Build-InlineTable -JsonPath ($dataDir "not_updated.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_not_updated_$timestamp.csv"         $tblTaskDis = Build-InlineTable -JsonPath ($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 "perm_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_perm_failures_$timestamp.csv"         $tblUpdated = Build-InlineTable -JsonPath ($dataDir "updated_devices.json de asociere") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_updated_devices_$timestamp.csv"         $tblActionReq = Build-InlineTable -JsonPath ($dataDir "action_required.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_action_required_$timestamp.csv"         $tblUnderObs = Build-InlineTable -JsonPath ($dataDir de asociere "under_observation.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_under_observation_$timestamp.csv"         $tblNeedsReboot = Build-InlineTable -JsonPath ($dataDir "needs_reboot.json de unire") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_needs_reboot_$timestamp.csv"         $tblSBOff = Build-InlineTable -JsonPath ($dataDir "secureboot_off.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_secureboot_off_$timestamp.csv"         $tblRolloutIP = Build-InlineTable -JsonPath ($dataDir "rollout_inprogress.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_rollout_inprogress_$timestamp.csv"         # Tabel particularizat pentru actualizare în așteptare - include coloanele UEFICA2023Status și UEFICA2023Error         $tblUpdatePending = ""         $upJsonPath = Join-Path $dataDir "update_pending.json"         if (Test-Cale $upJsonPath) {             încercați {                 $upData = Get-Content $upJsonPath -Brut | ConvertFrom-Json                 $upCount = $upData.Count                 dacă ($upCount -gt 0) {                     $upHeader = "<div style='margin:5px 0; font-size:.85em; color:#666'>Total: dispozitive $($upCount.ToString("N0")) | <a href='SecureBoot_update_pending_$timestamp.csv' style='color:#1a237e; font-weight:bold'>&#128196; Descărcați fișierul CSV complet pentru Excel><4 /a></div>"                     $upRows = ""                     $upSlice = $upData | Select-Object - Primul $maxInlineRows                     foreach ($d în $upSlice) {                         $uefiSt = dacă ($d.UEFICA2023Status) { $d.UEFICA2023Status } altfel, { "<span style="color:#999">null><0 /span>' }                         $uefiErr = dacă ($d.UEFICA2023Error) { "<span style='color:#dc3545'>$($d.UEFICA2023Error)</span>" } else { '-' }                         $policyVal = dacă ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } else { '-' }                         $wincsVal = dacă ($d.WinCSKeyApplied) { '<span class="badge badge-success">Yes><8 /span>' } else { '-' }                         $upRows += "<tr><td><3 $($d.HostName)</td><td><7 $($d.WMI_Manufacturer)</td><td><1 $($d.WMI_Model)</td><td><5 $uefiSt><6 /td><td><9 $uefiErr><50 /td><td><53 $policyVal><54 /td><td><57 $wincsVal><58 /td><td style='font-size:.75em'>$($d.BucketId)</td></tr><65 'n"                     }                     $upShowing = [matematică]::Min($maxInlineRows, $upCount)                     $upDevHeader = "<h3 style='font-size:.95em; culoare:#333; margin:10px 0 5px'>Device Details (showing first $upShowing)</h3>"                     $upTable = "<div style='max-height:500px; overflow-y:auto'><tabel><thead><tr><><9 HostName><0 /th><><3 Manufacturer><4 /th><th><7 Model><8 /th><th><1 UEFICA2023Status><2 /th><th><5 UEFICA2 023Eroare><6 /th><><9</th><><><BucketId>BucketId</th></tr></thead><body><5 $upRows><6 /tbody></table></div>"                     $tblUpdatePending = "$upHeader$upDevHeader$upTable"                 } altfel, {                     $tblUpdatePending = "<div style='padding:20px; culoare:#888; font-style:italic'>Niciun dispozitiv din această categorie.</div>"                 }             } captură {                 $tblUpdatePending = "<div style='padding:20px; culoare:#888; font-style:italic'>Niciun dispozitiv din această categorie.</div>"             }         } altfel, {             $tblUpdatePending = "<div style='padding:20px; culoare:#888; font-style:italic'>Niciun dispozitiv din această categorie.</div>"         }                  # Numărătoarea inversă de expirare a certificatului         $certToday = Get-Date         $certKekExpiry = [dată-oră]"2026-06-24"         $certUefiExpiry = [dată-oră]"2026-06-27"         $certPcaExpiry = [dată-oră]"2026-10-19"         $daysToKek = [matematică]:Max(0; ($certKekExpiry - $certToday). Zile)         $daysToUefi = [matematică]:Max(0; ($certUefiExpiry - $certToday). Zile)         $daysToPca = [matematică]:Max(0; ($certPcaExpiry - $certToday). Zile)         $certUrgency = dacă ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } altceva { '#28a745' }                  # Build manufacturer chart data inline (Top 10 by device count)         $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descendent | Select-Object -Primul 10         $mfrChartTitle = dacă ($stMfrCounts.Count -le 10) { "De producător" } altceva { "Primii 10 producători" }         $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_. Cheie)'" }) -asociere ","         $mfrUpdated = ($mfrSorted | ForEach-Object { $_. Value.Updated }) -join ","         $mfrUpdatePending = ($mfrSorted | ForEach-Object { $_. Value.UpdatePending }) -join ","         $mfrHighConf = ($mfrSorted | ForEach-Object { $_. Value.HighConf }) - asociere ","         $mfrUnderObs = ($mfrSorted | ForEach-Object { $_. Value.UnderObs }) -join ","         $mfrActionReq = ($mfrSorted | ForEach-Object { $_. Value.ActionReq }) -join ","         $mfrTempPaused = ($mfrSorted | ForEach-Object { $_. Value.TempPaused }) - unire ","         $mfrNotSupported = ($mfrSorted | ForEach-Object { $_. Value.NotSupported }) -join ","         $mfrSBOff = ($mfrSorted | ForEach-Object { $_. Value.SBOff }) -join ","         $mfrWithErrors = ($mfrSorted | ForEach-Object { $_. Value.WithErrors }) -join ","                  # Build manufacturer table         $mfrTableRows = ""         $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descendent | 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 cap < <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> titlul <><9 tabloul de bord cu starea certificatului de bootare securizat><0 /title><1 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script><5 ><7 stil < *{box-sizing:border-box; margine:0; spațiere:0} body{font-family:'Segoe UI',Tahoma,sans-serif; background:#f0f2f5; culoare:#333} .header{background:linear-gradient(135deg,#1a237e,#0d47a1); culoare:#fff; padding:20px 30px} .header h1{font-size:1.6em; margin-bottom:5px} .header .meta{font-size:.85em; opacitate:.9} .container{max-width:1400px; margin:0 auto; spațiere:20px} .cards{display:grid; grid-template-columns:repeat(auto-fill,minmax(170px,1fr)); spațiu liber:12px; margin:20px 0} .card{background:#fff; rază bordură:10px; spațiere:15px; box-shadow:0 2px 8px rgba(0,0,0,.08); border-left:4px solid #ccc;transition:transform .2s} .card:hover{transform:translateY(-2px); box-shadow:0 4px 15px rgba(0,0,0,.12)} .card .value{font-size:1.8em; font-weight:700} .card .label{font-size:.8em; culoare:#666; margin-top:4px} .card .pct{font-size:.75em; culoare:#888} .section{background:#fff; rază bordură:10px; spațiere:20px; margin:15px 0; box-shadow:0 2px 8px rgba(0,0,0,.08)} .section h2{font-size:1.2em; culoare:#1a237e; margin-bottom:10px; cursor:pointer; user-select:none} .section h2:hover{text-decoration:underline} .section-body{display:none} .section-body.open{display:block} .charts{display:grid; grid-template-columns:1fr 1fr; spațiu liber:20px; margin:20px 0} .chart-box{background:#fff; rază bordură:10px; spațiere:20px; box-shadow:0 2px 8px rgba(0,0,0,.08)} tabel{lățime:100%; border-collapse:collapse; font-size:.85em} th{background:#e8eaf6; padding:8px 10px; aliniere text:la stânga; poziție:adeziv; primele:0; index z:1} td{padding:6px 10px; border-bottom:1px solid #eee} tr:hover{background:#f5f5f5} .badge{display:inline-block; spațiere:2px 8px;rază-bordură:10px; font-size:.75em; font-weight:700} .badge-success{background:#d4edda; culoare:#155724} .badge-danger{background:#f8d7da; culoare:#721c24} .badge-warning{background:#fff3cd; culoare:#856404} .badge-info{background:#d1ecf1; culoare:#0c5460} .top-link{float:right; font-size:.8em; culoare:#1a237e; text-decoration:none} .footer{text-align:center; spațiere:20px; culoare:#999; font-size:.8em} a{color:#1a237e} ><9 </style ></cap >corp < <div class="header">     <h1>tabloul de bord cu starea certificatului de bootare securizat</h1>     <div class="meta">generat: $($stats. ReportGeneratedAt) | Total dispozitive: $($c.Total.ToString("N0")) | Bucketuri unice: $($stAllBuckets.Count)</div><3 ><5 </div <div class="container">

Fișe KPI<!-- - se poate face clic, se poate face clic pe secțiuni --> <div class="cards">     <o clasă="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#dc3545; text-decoration:none; position:relative"><div style="position:absolute; top:8px; dreapta:8px; background:#dc3545; culoare:#fff; padding:1px 6px; rază bordură:8px; font-size:.65em; font-weight:700">PRIMARY</div><div class="value" style="color:#dc3545">$($stNotUptodate.ToString(") N0"))</div><div class="label">NOT UPDATED><6 /div><div class="pct">$($stats. PercentNotUptodate)% - NEEDS ACTION><0 /div></a><3     <o clasă="card" href="#s-upd" onclick="openSection('d-upd')" style="border-left-color:#28a745; text-decoration:none; position:relative"><div style="position:absolute; top:8px; dreapta:8px; background:#28a745; culoare:#fff; padding:1px 6px; rază bordură:8px; font-size:.65em; font-weight:700">PRIMARY><8 /div><div class="value" style="color:#28a745">$($c.Updated.ToString(") N0"))</div><div class="label">Actualizat><6 /div><div class="pct">$($stats. PercentCertUpdated)%</div></a><3     <o clasă="card" href="#s-sboff" onclick="openSection('d-sboff')" style="border-left-color:#6c757d; text-decoration:none; position:relative"><div style="position:absolute; top:8px; dreapta:8px; background:#6c757d; culoare:#fff; padding:1px 6px; rază bordură: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">necesită repornirea><2 /div><clasa div="pct">$(if($c.Total -gt 0){[matematică]:Round(($c.NeedsReboot/$c.Total)*100,1)}else{0})% - așteaptă repornirea><6 /div></a><9     <o clasă="card" href="#s-upd-pend" onclick="openSection('d-upd-pend')" style="border-left-color:#6f42c1; text-decoration:none"><div class="value" style="color:#6f42c1">$($c.UpdatePending.ToString("N0"))</div><div class="label">Update Pending</div><div class="pct">$(if($c.Total -gt 0){[math]:Round(($c.UpdatePending/$c.Total)*100,1)}else{0})% - Politica/WinCS aplicată, se așteaptă actualizarea><2 /div></a><5     <o clasă="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){[matematică]:Round(($c.RolloutInProgress/$c.Total)*100;1)}else{0})%</div></a><11     <o clasă="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#28a745; text-decoration:none"><div class="value" style="color:#28a745">$($c.HighConf.ToString("N0"))</div><div class="label">high confidence><20 /div><div class="pct">$($stats. PercentHighConfidence)% - sigur pentru lansarea><24 /div></a><27     <o clasă="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><1div class="pct"><9 $(if($c.Total -gt 0){[matematică]:Round(($c.UnderObs/$c.Total)*100,1)}else{0})%</div></a><3     <a class="card" href="#s-ar" onclick="openSection('d-ar')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.ActionReq.ToString("N0"))</div><div class="label">Action Required><2 /div><div class="pct">$($stats. PercentActionRequired)% - trebuie să testați><6 /div></a><9     <o clasă="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)% - Similar cu nereușit><2 /div></a><5     <o clasă="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){[matematică]:Round(($c.TaskDisabled/$c.Total)*100,1)}else{0})% - Blocat><8 /div></a><91     <o clasă="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. În pauză</div><div class="pct">$(if($c.Total -gt 0){[matematică]:Round(($c.TempPaused/$c.Total)*100,1)}else{0})%</div></a>     <o clasă="card" href="#s-ki" onclick="openSection('d-ki')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.WithKnownIssues.ToString("N0"))</div><div class="label">Known Issues><6 /div><div class="pct">$(if($c.Total -gt 0){[math]:Round(($c.WithKnownIssues/$c.Total)*100,1)}else{0})%</div></a><3     <o clasă="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>     <o clasă="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)% - erori UEFI</div></a>     ><6 o clasă="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. Erori</div><div class="pct">$(if($c.Total -gt 0){[math]:Round(($c.TempFailures/$c.Total)*100,1)}else{0})%</div></a>     <o clasă="card" href="#s-pf" onclick="openSection('d-pf')" style="border-left-color:#721c24; text-decoration:none"><div class="value" style="color:#721c24">$($c.PermFailures.ToString("N0"))</div><div class="label">Not Supported><6 /div><div class="pct">$(if($c.Total -gt 0){[matematică]:Round(($c.PermFailures/$c.Total)*100,1)}else{0})%</div></a><3 </div>

<!-- de expirare a vitezei de implementare & certificat --> <div id="s-velocity" style="display:grid; grid-template-columns:1fr 1fr; spațiu liber:20px; margin:15px 0"> <div class="section" style="margin:0">     <h2>&#128197; Viteza de implementare</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">dispozitive actualizate din $($c.Total.ToString("N0"))</div>         <div style="margin:10px 0; background:#e8eaf6; înălțime:20px; rază bordură:10px; overflow:hidden"><div style="background:#28a745; înălțime:100%; lățime:$($stats. PercentCertUpdated)%; border-radius:10px"></div></div>         <div style="font-size:.8em; color:#888">$($stats. PercentCertUpdated)% complet</div>         <div style="margin-top:10px; spațiere:10px; background:#f8f9fa; rază bordură:8px; font-size:.85em">             <div><dispozitive puternice>Rămase:</strong> $($stNotUptodate.ToString("N0")) necesită acțiuni</div>             <div><dispozitive puternice>Blocking:</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated) (erori + activități permanente + dezactivate)</div>             <div><dispozitive puternice>Sigur de implementat:</strong> $($stSafeList.ToString("N0")) dispozitive (același bucket ca reușit)</div>             $velocityHtml        ></div    ></div ></div <div class="section" style="margin:0; border-left:4px solid #dc3545">     <h2 style="color:#dc3545">&#9888; numărătoarea inversă a expirării certificatului</h2>     <div class="section-body open">         <div style="display:flex; spațiu liber:15px; margin-top:10px">             <div style="text-align:center; spațiere:15px; rază bordură:8px; lățime minimă:120px; background:linear-gradient(135deg,#fff5f5,#ffe0e0); border:2px solid #dc3545; flex:1">                 <div style="font-size:.65em; culoare:#721c24; transformare text:majuscule; font-weight:bold">&#9888; FIRST TO EXPIRE</div>                 ><4 div style="font-size:.85em; font-weight:bold; culoare:#dc3545; margin:3px 0"><5 KEK CA 2011</div>                 ><8 div id="daysKek" style="font-size:2.5em; font-weight:700; culoare:#dc3545; line-height:1"><9 $daysToKek</div>                 ><2 div style="font-size:.8em; color:#721c24"><3 zile (24 iunie 2026)><4 /div>             ><6 /div>             ><8 div style="text-align:center; spațiere:15px; rază bordură:8px; lățime minimă:120px; background:linear-gradient(135deg,#fffef5,#fff3cd); border:2px solid #ffc107; flex:1"><9                 <div style="font-size:.65em; culoare:#856404; transformare text:majuscule; font-weight:bold">UEFI CA 2011</div>                 <div id="daysUefi" style="font-size:2.2em; font-weight:700; culoare:#856404; înălțime linie:1; margin:5px 0">$daysToUefi</div>                 <div style="font-size:.8em; color:#856404">zile (27 iunie 2026)</div>            ></div             <div style="text-align:center; spațiere:15px; rază bordură:8px; lățime minimă:120px; background:linear-gradient(135deg,#f0f8ff,#d4edff); border:2px solid #0078d4; flex:1">                 <div style="font-size:.65em; culoare:#0078d4; transformare text:majuscule; font-weight:bold">Windows PCA</div>                 <div id="daysPca" style="font-size:2.2em; font-weight:700; culoare:#0078d4; înălțime linie:1; margin:5px 0">$daysToPca><2 /div><3                 <div style="font-size:.8em; color:#0078d4">zile (19 octombrie 2026)</div><7            ><9 </div        ><1 </div         <div style="margin-top:15px; spațiere:10px; background:#f8d7da; rază bordură:8px; font-size:.85em; border-left:4px solid #dc3545">             <>&#9888; CRITIC:</strong> Toate dispozitivele trebuie actualizate înainte de expirarea certificatului. Dispozitivele care nu au fost actualizate până la termenul limită nu pot aplica actualizări de securitate viitoare pentru Manager de bootare și Bootare sigură după expirare.></div    ></div ></div > </div

diagrame<!-- --> <div class="charts">     <div class="chart-box"><h3>Deployment Status</h3><canvas id="deployChart" height="200"></canvas></div><5     <div class="chart-box"><h3><9 $mfrChartTitle</h3><canvas id="mfrChart" height="200"></canvas></div> > </div

$(dacă ($historyData.Count -ge 1) { "<!-- Diagramă tendințe istorice --> <div class='section'>     <h2 onclick='"toggle('d-trend')'">&#128200; Actualizați progresul în timp <o clasă='top-link' href='#'>&#8593; Primele</a></h2>     <div id='d-trend' class='section-body open'>         id pânză <='trendChart' height='120'></canvas>         <div style='font-size:.75em; culoare:#888; margin-top:5px'>Linii solide = date reale$(if ($historyData.Count -ge 2) { " | Linie întreruptă = proiectat (dublare exponențială: 2&#x2192;4&#x2192;8&#x2192;16... dispozitive pe undă)" } altceva { " | Rulați din nou agregarea mâine pentru a vedea liniile de tendință și proiecția" })</div>    ></div </div>" })

Descărcări<!-- CSV - > <div class="section">     <h2 onclick="toggle('dl-csv')">&#128229; Descărcați Date complete (CSV pentru Excel) <a class="top-link" href="#">Top</a></h2><2     <div id="dl-csv" class="section-body open" style="display:flex; încadrare flexibilă:încadrare; gap:5px">         <a href="SecureBoot_not_updated_$timestamp.csv" style="display:inline-block; background:#dc3545; culoare:#fff; padding:6px 14px; rază bordură:5px; text-decoration:none; font-size:.8em">Nedate ($($stNotUptodate.ToString("N0")))</a><8         <a href="SecureBoot_errors_$timestamp.csv" style="display:inline-block; background:#dc3545; culoare:#fff; padding:6px 14px; rază bordură:5px; text-decoration:none; font-size:.8em">Errors ($($c.WithErrors.ToString("N0")))</a>         <a href="SecureBoot_action_required_$timestamp.csv" style="display:inline-block; background:#fd7e14; culoare:#fff; padding:6px 14px; rază bordură:5px; text-decoration:none; font-size:.8em">Acțiune necesară ($($c.ActionReq.ToString("N0"))</a>         <a href="SecureBoot_known_issues_$timestamp.csv" style="display:inline-block; background:#dc3545; culoare:#fff; padding:6px 14px; rază bordură:5px; text-decoration:none; font-size:.8em">Probleme cunoscute ($($c.WithKnownIssues.ToString("N0"))</a>         <a href="SecureBoot_task_disabled_$timestamp.csv" style="display:inline-block; background:#dc3545; culoare:#fff; padding:6px 14px; rază bordură: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; background:#28a745; culoare:#fff; padding:6px 14px; rază bordură:5px; text-decoration:none; font-size:.8em">actualizat ($($c.Updated.ToString("N0")))</a>         <a href="SecureBoot_Summary_$timestamp.csv" style="display:inline-block; background:#6c757d; culoare:#fff; padding:6px 14px; rază bordură:5px; text-decoration:none; font-size:.8em">Summary</a>         <div style="width:100%; font-size:.75em; culoare:#888; margin-top:5px">fișiere CSV deschise în Excel. Disponibil atunci când este găzduit pe web server.</div>    ></div > </div

Defalcarea producătorului<!-- - > <div class="section">     <h2 onclick="toggle('mfr')">de producător <a class="top-link" href="#">Top</a></h2><1     <div id="mfr" class="section-body open">     <tabel><><><><1 producătorul><2><><5><6 total><><9 actualizate><9><0><><3><4><><7 acțiune necesară><8 /th></tr></thead><3     <întruchiparea><5 $mfrTableRows><6 /tbody></table><9    ><1 </div </div>

<!-- secțiunile dispozitivului (primele 200 de descărcări în linie + CSV) - > <div class="section" id="s-err">     <h2 onclick="toggle('d-err')">&#128308; Dispozitivele cu erori ($($c.WithErrors.ToString("N0"))) <a class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-err" class="section-body">$tblErrors</div> ></div <div class="section" id="s-ki">     <h2 onclick="toggle('d-ki')" style="color:#dc3545">&#128308; Probleme cunoscute ($($c.WithKnownIssues.ToString("N0"))) <a class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-ki" class="section-body">$tblKI</div> ></div <div class="section" id="s-kek">     <h2 onclick="toggle('d-kek')">&#128992; KEK lipsă - evenimentul 1803 ($($c.WithMissingKEK.ToString("N0"))) <o clasă="top-link" href="#">&#8593;<de sus /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; Acțiune necesară ($($c.ActionReq.ToString("N0"))) <o clasă="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; Sub Observație ($($c.UnderObs.ToString("N0"))) <o clasă="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; Nedate ($($stNotUptodate.ToString("N0"))) <o clasă="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; Activitate dezactivată ($($c.TaskDisabled.ToString("N0"))) >&#8593; 5 o clasă="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; Erori temporare ($($c.TempFailures.ToString("N0"))) <a class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-tf" class="section-body">$tblTemp</div> ></div <div class="section" id="s-pf">     <h2 onclick="toggle('d-pf')" style="color:#721c24">&#128308; Erori permanente / Neacceptat ($($c.PermFailures.ToString("N0"))) <a class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-pf" class="section-body">$tblPerm</div> ></div <div class="section" id="s-upd-pend">     <h2 onclick="toggle('d-upd-pend')" style="color:#6f42c1">&#9203; Actualizare în așteptare ($($c.UpdatePending.ToString("N0"))) - Politică/WinCS aplicată, Se așteaptă Actualizarea <a class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-upd-pend" class="section-body"><p style="color:#666; margin-bottom:10px">Dispozitivele la care se aplică cheia AvailableUpdatesPolicy sau WinCS, dar UEFICA2023Status este încă NotStarted, InProgress sau null.</p>$tblUpdatePending</div> ></div <div class="section" id="s-rip">     <h2 onclick="toggle('d-rip')" style="color:#17a2b8">&#128309; Lansare în curs ($($c.RolloutInProgress.ToString("N0"))) <o clasă="top-link" href="#">&#8593;<de sus /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"))) - În afara domeniului <a class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-sboff" class="section-body">$tblSBOff</div> ></div <div class="section" id="s-upd">     <h2 onclick="toggle('d-upd')" style="color:#28a745">&#128994; Dispozitive actualizate ($($c.Updated.ToString("N0"))) <o clasă="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; Actualizat - necesită repornire ($($c.NeedsReboot.ToString("N0"))) <a class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-nrb" class="section-body">$tblNeedsReboot</div> </div>

<div class="footer">tabloul de bord de implementare a certificatului de bootare securizat | $($stats) generat. ReportGeneratedAt) | StreamingMode | Memorie vârf: ${stPeakMemMB} MB</div> </div><!-- /container -->

>scriptului< comutare funcție(id){var e=document.getElementById(id); e.classList.toggle('open')} function openSection(id){var e=document.getElementById(id); if(e&&!e.classList.contains('open')){e.classList.add('open')}} new Chart(document.getElementById('deployChart'),{type:'doughnut',data:{labels:['Updated','Update Pending','High Confidence','Under Observation','Action Required','Temp. Paused','Not Supported','SecureBoot OFF','With Errors'],datasets:[{data:[$($c.Updated),$($c.UpdatePending),$($c.HighConf),$($c.UnderObs),$($c.ActionReq),$($c.TempPaused),$($c.NotSupported),$($c.SBOff),$($c.WithErrors)],backgroundColor:['#28a745','#6f42c1','#20c997','#17a2b8','#fd7e14','#6c757d','#6c757d', '#721c24','#adb5bd','#dc3545']}]},options:{responsive:true,plugins:{legend:{position:'right',labels:{font:{size:11}}}}}}); new Chart(document.getElementById('mfrChart'),{type:'bar',data:{labels:[$mfrLabels],datasets:[{label:'Updated',data:[$mfrUpdated],backgroundColor:'#28a745'},{label:'Update Pending',data:[$mfrUpdatePending],backgroundColor:'#6f42c1'},{label:'High Confidence',data:[$mfrHighConf],backgroundColor:'#20c997'},{label:'Under Observation',data:[$mfrUnderObs],backgroundColor:'#17a2b8'},{label:'Action Required',data:[$mfrActionReq],backgroundColor:'#fd7e14'},{ etichetă:'Temp. Paused',data:[$mfrTempPaused],backgroundColor:'#6c757d'},{label:'Not Supported',data:[$mfrNotSupported],backgroundColor:'#721c24'},{label:'SecureBoot OFF',data:[$mfrSBOff],backgroundColor:'#adb5bd'},{label:'With Errors',data:[$mfrWithErrors],backgroundColor:'#dc3545'}]},options:{responsive:true,scales:{x:{stacked:true},y:{stacked:true}},plugins:{legend:{position:'top'}}}}); Diagramă tendințe istorice 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); } seturi de date 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 (dublare 2x)',date:projLine,borderColor:'#28a745',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'});     datasets.push({label:'Projected Not Updated',data:projNotUpdLine,borderColor:'#dc3545',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}); } new Chart(document.getElementById('trendChart'),{type:'line',data:{labels:allLabels,datasets:datasets},options:{responsive:true,scales:{y:{beginAtZero:true,title:{display :adevărat,text:'Devices'}},x:{title:{display:true,text:'Date'}}},pluginuri:{legendă:{position:'top'},title:{display:true,text:'Secure Boot Update Progress over Time'}}}} ); } Numărătoarea inversă dinamică (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))}))(); ></script ></corp > </html "@         [System.IO.File]::WriteAllText($htmlPath, $htmlContent, [System.Text.UTF8Encoding]::new($false))         # Păstrați întotdeauna o copie stabilă "Cea mai recentă", astfel încât administratorii nu au nevoie pentru a urmări timestamps         $latestPath = Join-Path $OutputPath "SecureBoot_Dashboard_Latest.html"         Copy-Item $htmlPath $latestPath - Forțare         $stTotal = $streamSw.Elapsed.TotalSeconds         # Salvați manifestul fișierului pentru modul incremental (detectare rapidă fără modificări la următoarea rulare)         dacă ($IncrementalMode -sau $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 "Se salvează manifestul fișierului pentru modul incremental..." -Prim-planColor gri             foreach ($jf în $jsonFiles) {                 $stNewManifest[$jf. NumeComplet.ToLowerInvariant()] = @{                     LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o")                     Dimensiune = $jf. Lungime                 }             }             Save-FileManifest $stNewManifest -Cale manifest $stManifestPath             Write-Host " Manifest salvat pentru fișiere $($stNewManifest.Count) " -ForegroundColor DarkGray         }         # CURĂȚARE RETENȚIE         # Orchestrator folder reutilizabil (Aggregation_Current): păstrați doar cea mai recentă rula (1)         # Admin rulează manual / alte foldere: să păstreze ultimele 7 rulează         # REZUMAT CSV-uri nu sunt șterse - acestea sunt mici (~ 1 KB) și sunt sursa de backup pentru istoricul tendințelor         $outputLeaf = Split-Path $OutputPath - Frunză         $retentionCount = dacă ($outputLeaf -eq 'Aggregation_Current') { 1 } altfel { 7 }         # Prefixe de fișier sigure pentru a curăța (instantanee efemere 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_"         )         # Găsiți toate marcajele de timp unice doar din fișierele care se pot curăța         $cleanableFiles = Get-ChildItem $OutputPath -Fișier -EA SilentlyContinue |             Where-Object { $f = $_. Nume; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Count -gt 0 }         $allTimestamps = @($cleanableFiles | ForEach-Object {             dacă ($_. Nume -potrivire '(\d{8}-\d{6})') { $Matches[1] }         } | Sort-Object -Unic -descendent)         dacă ($allTimestamps.Count -gt $retentionCount) {             $oldTimestamps = $allTimestamps | Select-Object - Ignorare $retentionCount             $removedFiles = 0; $freedBytes = 0             foreach ($oldTs în $oldTimestamps) {                 pentru a căuta ($prefix în $cleanupPrefixes) {                     $oldFiles = Get-ChildItem $OutputPath - Fișier - Filtru "${prefix}${oldTs}*" -EA SilentlyContinue                     pentru a căuta ($f în $oldFiles) {                         $freedBytes += $f.Lungime                         Remove-Item $f.FullName -Force -EA SilentlyContinue                         $removedFiles++                     }                 }             }             $freedMB = [matematică]:Round($freedBytes / 1 MB, 1)             Write-Host "Curățare reținere: s-au eliminat fișierele $removedFiles din rulările vechi $($oldTimestamps.Count), s-au eliberat ${freedMB} MB (se păstrează ultima $retentionCount + toate CSV-urile Summary/NotUptodate)" -ForegroundColor DarkGray         }         Write-Host "'n$("=" * 60)" -Prim planColor Cyan         Write-Host "STREAMING AGGREGATION COMPLETE" -ForegroundColor Green         Write-Host ("=" * 60) -Prim-planColor Cyan         Write-Host " Total dispozitive: $($c.Total.ToString("N0"))" -Prim planColor Alb         Write-Host " NEDATE: $($stNotUptodate.ToString("N0")) ($($stats. PercentNotUptodate)%)" -Prim-planColor $(dacă ($stNotUptodate -gt 0) { "Galben" } altceva { "Verde" })         Write-Host " Actualizat: $($c.Updated.ToString("N0")) ($($stats. PercentCertUpdated)%)" -Prim-planColor verde         Write-Host " Cu erori: $($c.WithErrors.ToString("N0"))" -ForegroundColor $(if ($c.WithErrors -gt 0) { "Red" } else { "Green" })         Write-Host " Memorie vârf: ${stPeakMemMB} MB" -ForegroundColor Cyan         Write-Host " Oră: $([matematică]:Round($stTotal/60,1)) min" -Culoare prim plan alb         Write-Host " Tablou de bord: $htmlPath" - Culoare prim plan alb         return [PSCustomObject]$stats     }     #endregion MODUL DE REDARE ÎN FLUX } altfel, {     Write-Error "Calea de intrare nu s-a găsit: $InputPath"     ieșire 1 }                                                      

Aveți nevoie de ajutor suplimentar?

Doriți mai multe opțiuni?

Explorați avantajele abonamentului, navigați prin cursurile de instruire, aflați cum să vă securizați dispozitivul și multe altele.